People need to see the buy and sell quantities currently listed at each price.
Updated September 10, 2026
Problem
A last-traded price does not show the quantities currently listed for buying and selling at nearby prices.
Solution
Show buy and sell orders in separate, clearly labeled price ladders, with quantity at each level and the spread between them. A depth chart can show cumulative listed quantity across prices.
Keep rows stable enough to read as quantities update. Use brief change cues sparingly and let people group nearby prices when there is too much detail.
Label both sides independently of color. Show feed age and connection problems clearly. Make the scope of the displayed book clear so it is not mistaken for all available market activity.
Use when
People need to inspect changing listed quantities at specific prices.
Don’t use when
The audience is not trading. On a monitoring or reporting dashboard, depth is detail nobody acts on, and a spread or liquidity metric says the same thing in a row.
Trade-offs
Rapid updates make sustained reading difficult and can strain the client. Delayed data may look like a quiet market. The visible book is incomplete and listed orders can change, so its shape should not imply guaranteed availability.
Checklist
What is the update rate, and can a person actually read at it?
Do rows hold position while values change, or does the ladder reorder?
Are changes marked in a way that decays, so recent activity is visible?
Is price aggregation adjustable, and is the current increment displayed?
Is the spread visually distinct rather than merely implied by the gap?
Is cumulative depth available alongside per-level quantity?
Does the display say when the feed is delayed, throttled or disconnected?
Does side depend on colour alone, and does it survive colour vision deficiency?
Is the colour convention right for this market’s audience?
Does anything indicate what the book cannot see?
Compare
Bloomberg and Refinitiv terminals set the conventions most other implementations inherit, optimised for a trained full-time operator and unapologetic about density. Crypto exchanges brought the pattern to a mass audience and generally pair the ladder with a depth chart by default, which is the clearest teaching device for what the ladder means. Trading platforms with a DOM ladder let orders be placed directly on the price rows, which turns the display from a readout into a control surface and raises the stakes on every rendering decision. General dashboard tools have no equivalent, and a Grafana or Datadog rendering of a book is a table refreshing too slowly to trade on, which is a fair summary of why this pattern stayed inside specialist software.
Related
Log tail shares the challenge of rapidly arriving information. Data table provides the underlying row format. Red/green direction coloring covers color conventions.
Wallboard mode addresses shared viewing. Time series shows changes over time rather than the current ladder.
Wireframe — the pattern's anatomy, not any one product's version of it
Implementation
The densest display in this collection and the one with the most trained audience. Depth is drawn as a background fill behind the numbers so the shape and the values occupy the same pixels.
OrderBook.tsxTwo ladders keyed by price with the depth fill behind each row, a wall detected and clamped, a changed size that flashes, and the cumulative curve beside it.
import { useEffect, useRef, useState } from "react";import { cn } from "@/lib/utils";export type Level = { price: number; size: number };type Props = { /** Best first on both sides: asks ascending, bids descending. */ asks: Level[]; bids: Level[]; /** The price increment levels are aggregated to. Shown, because a ladder at * 0.01 and one at 0.10 are different books. */ increment: number; feed: "live" | "delayed" | "disconnected"; /** Side by position, colour second. Markets that invert red and green flip * this and nothing else. */ upIsBid?: boolean;};const qty = (n: number) => n.toLocaleString("en-US");const median = (xs: number[]) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)] ?? 0;const cumulative = (side: Level[]) => side.reduce<number[]>((acc, l) => [...acc, (acc[acc.length - 1] ?? 0) + l.size], []);export function OrderBook({ asks, bids, increment, feed, upIsBid = true }: Props) { const decimals = Math.max(0, -Math.floor(Math.log10(increment))); const px = (p: number) => p.toFixed(decimals); const spread = asks[0].price - bids[0].price; const mid = (asks[0].price + bids[0].price) / 2; // A wall is a level holding far more than its neighbours. It gets the strong // fill and is clamped, so the rest of the ladder keeps its scale. const isWall = (l: Level, side: Level[]) => l.size > 5 * median(side.map((s) => s.size)); const full = 2 * Math.max(...[...asks, ...bids].filter((l) => !isWall(l, asks) && !isWall(l, bids)).map((l) => l.size)); // Rows hold position by price. A size that changed since the last render // flashes and decays; the ladder never reorders under the eye. const prev = useRef(new Map<number, number>()); const [flash, setFlash] = useState<Set<number>>(new Set()); useEffect(() => { const changed = new Set<number>(); for (const l of [...asks, ...bids]) if (prev.current.has(l.price) && prev.current.get(l.price) !== l.size) changed.add(l.price); prev.current = new Map([...asks, ...bids].map((l) => [l.price, l.size])); if (changed.size) { setFlash(changed); const t = setTimeout(() => setFlash(new Set()), 700); return () => clearTimeout(t); } }, [asks, bids]); const row = (l: Level, side: "ask" | "bid") => { const wall = isWall(l, side === "ask" ? asks : bids); const up = (side === "bid") === upIsBid; return ( <li key={l.price} className={cn("relative flex justify-between py-1 transition-colors", flash.has(l.price) && "bg-accent")}> <span className={cn("absolute inset-y-0 right-0", up ? (wall ? "bg-direction-up/40" : "bg-direction-up/10") : (wall ? "bg-direction-down/40" : "bg-direction-down/10"))} style={{ width: `${Math.min(100, (l.size / full) * 100)}%` }} /> <span className={cn("relative", up ? "text-direction-up" : "text-direction-down")}>{px(l.price)}</span> <span className={cn("relative tabular-nums", wall ? "font-semibold text-card-foreground" : "text-card-foreground")}>{qty(l.size)}</span> </li> ); }; // Depth: cumulative size against price, mirrored about one baseline at // the mid. Bids stack upward to the left, asks downward to the right, so the // two sides read as one shape and a wall is a step on either arm. const W = 240, H = 176, Y0 = H / 2; // The wall is clamped here the same way the ladder clamps it, so one // level cannot flatten the other arm. The ladder still says how big it is. const capped = (side: Level[]) => { const cap = 5 * median(side.map((s) => s.size)); return side.map((l) => ({ ...l, size: Math.min(l.size, cap) })); }; const cumB = cumulative(capped(bids)), cumA = cumulative(capped(asks)); const maxCum = Math.max(cumB[cumB.length - 1], cumA[cumA.length - 1]); const lo = bids[bids.length - 1].price, hi = asks[asks.length - 1].price; const X = (p: number) => 4 + ((p - lo) / (hi - lo)) * (W - 8); const rise = (c: number) => (c / maxCum) * (Y0 - 10); const line = (side: Level[], cum: number[], dir: 1 | -1) => `M${X(mid).toFixed(1)} ${Y0} ` + side.map((l, i) => `L${X(l.price).toFixed(1)} ${(Y0 - dir * rise(cum[i])).toFixed(1)}`).join(" "); const area = (side: Level[], cum: number[], dir: 1 | -1) => `${line(side, cum, dir)} L${X(side[side.length - 1].price).toFixed(1)} ${Y0} Z`; // Spelled out in full: Tailwind only emits a class it can read verbatim. const UP = { fill: "fill-direction-up/20", stroke: "stroke-direction-up" }; const DOWN = { fill: "fill-direction-down/20", stroke: "stroke-direction-down" }; const bidTone = upIsBid ? UP : DOWN; const askTone = upIsBid ? DOWN : UP; return ( <div className="grid grid-cols-[1.2fr_1fr] gap-5"> <div className="rounded-lg border bg-card p-4 font-mono"> <div className="flex items-baseline justify-between border-b pb-2 font-sans text-[11px] uppercase tracking-wide text-muted-foreground"> <span>Price</span> <span className={cn("normal-case", feed !== "live" && "text-status-warn")}>{feed} · {px(increment)} steps</span> <span>Size</span> </div> <ul className="mt-1 text-[11px]">{[...asks].reverse().map((l) => row(l, "ask"))}</ul> <p className="my-1.5 border-y py-1.5 text-[11px] text-muted-foreground">spread {px(spread)}</p> <ul className="text-[11px]">{bids.map((l) => row(l, "bid"))}</ul> </div> <div className="rounded-lg border bg-card p-4"> <p className="border-b pb-2 text-[11px] uppercase tracking-wide text-muted-foreground">Depth</p> <div className="overflow-x-auto"> <svg viewBox={`0 0 ${W} ${H + 14}`} width={W} height={H + 14} className="mt-3 block" role="img" aria-label="Cumulative depth by price"> <path d={area(bids, cumB, 1)} className={bidTone.fill} stroke="none" /> <path d={area(asks, cumA, -1)} className={askTone.fill} stroke="none" /> <path d={line(bids, cumB, 1)} fill="none" className={bidTone.stroke} strokeWidth="2" strokeLinejoin="round" /> <path d={line(asks, cumA, -1)} fill="none" className={askTone.stroke} strokeWidth="2" strokeLinejoin="round" /> <line x1={4} y1={Y0} x2={W - 4} y2={Y0} className="stroke-border" /> <line x1={X(mid)} y1={6} x2={X(mid)} y2={H - 4} className="stroke-border" strokeDasharray="3 3" /> <text x={X(mid)} y={H + 10} textAnchor="middle" className="fill-muted-foreground text-[9px] tabular-nums">{px(mid)}</text> </svg> </div> </div> </div> );}
demo.tsxHow it is called: four levels a side, a 0.02 spread, and an 18,400 wall at 104.00.
import { OrderBook } from "./OrderBook";/** * Four levels a side at 0.02 increments, a 0.02 spread, and a wall at 104.00 * holding eighteen thousand against neighbours in the hundreds. The wall is * the step in the depth curve. */const ASKS = [ { price: 104.06, size: 670 }, { price: 104.08, size: 1_580 }, { price: 104.1, size: 820 }, { price: 104.12, size: 1_240 },];const BIDS = [ { price: 104.04, size: 720 }, { price: 104.02, size: 1_010 }, { price: 104.0, size: 18_400 }, { price: 103.98, size: 610 },];export default function Demo() { return <OrderBook asks={ASKS} bids={BIDS} increment={0.02} feed="live" />;}
What it renders. Identical markup in both panes, with only the token values changing.
Screenshots are grouped by product and include the capture date.
Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.
Trade / BTC-USD September 11, 2026Kraken Pro, signed out (public market data)dense · dark · desktop-web
The order book separates asks and bids by position as well as red and green. A labeled spread sits between them and includes both an absolute value and a percentage. Depth bars behind the rows help compare quantities that are difficult to scan as long decimal values. A price-increment control changes how orders are grouped into levels.
Order book and depthBuy and sell ladders show quantities with depth bars and a labeled spread.