Skip to content
KONIGI

Dashboards / Visual representation / Order book and depth

13 of 22

Order book and depth

A trader needs to see supply and demand at every price right now.

Updated September 10, 2026

Problem

The last trade price is one number and it describes the past. What the trader needs is what is available right now: how much someone will buy at each price below, how much someone will sell at each price above, and how thin it gets as you move away from the middle.

Solution

Two stacked ladders sharing a price axis. Bids below, asks above, the spread between them, each row carrying price and resting quantity. Beside or behind it, a depth chart: cumulative quantity plotted against price, producing two curves that meet at the spread.

The encoding earns its density because the shape answers questions no single number can. A steep depth curve means size can be moved without shifting the price much. A shallow one means the next order eats several levels. A wall—one price level holding far more than its neighbours—is a visible intention, and whether it is real or bait is the kind of judgement the display exists to support.

The design constraint that dominates everything is update rate. This is the most volatile display in the collection: rows change many times a second, and a naive implementation redraws so aggressively that a human cannot read it at all. The conventions that emerged are all about making change legible rather than merely current. Rows hold position while their quantities change, so the ladder does not reorder underneath the eye. New and changed levels flash briefly and decay. Aggregation by price increment collapses the tail so the levels near the spread keep their space.

Colour here is inherited rather than designed. Bid and ask sides are conventionally green and red in Western markets, which collides directly with the accessibility problem covered under red/green direction coloring, and which is inverted in several Asian markets. Position on the ladder carries the same information and does it more reliably.

Use when

Someone is deciding size and price against live resting liquidity, and the shape of that liquidity changes faster than a chart can summarise.

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

The display demands sustained attention and punishes glancing, which makes it the opposite of nearly every other pattern here. It is functionally unreadable to anyone untrained, so it cannot appear on a shared screen without explanation. High update rates cost real client performance and can mislead when the feed is throttled or the client falls behind, because a stale book looks exactly like a calm one. And the visible book is not the whole market: hidden and iceberg orders mean the display is a partial picture presented with total confidence.

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.

Log tail is the other pattern built for continuous high-rate arrival, and it shares the pause-and-read problem. Data table is what this degrades into at a slower update rate. Red/green direction coloring covers the accessibility trap this display walks into by convention. Wallboard mode is the trading-floor context. Time series is the summarised view the book is the raw form of.

Order book and depth anatomy Two ladders sharing a price axis with the spread between them, and a depth chart beside it plotting cumulative quantity against price. One bid level holds far more than its neighbours, which shows as a step in the depth curve and as a wall on the ladder. Two ladders and the curve they imply Price Size 104.12 1,240 104.10 820 104.08 1,580 104.06 670 spread 0.02 104.04 720 104.02 1,010 104.00 18,400 103.98 610 1 2 Depth 104.03 3 1 THE SPREAD Bids one side, asks the other, sharing one price axis. Position carries the side more reliably than the inherited red and green, which invert in several Asian markets. 2 A WALL One level holding far more than its neighbours is a visible intention. Whether it's real or bait is the judgement the display exists to support. 3 THE STEP IN THE CURVE Steep means size moves without shifting price. Shallow means the next order eats several levels. The wall is the ledge between the two. Rows hold their position while quantities change, so the ladder never reorders under the eye. Change flashes and decays, rather than simply being current.
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.

Tokens
--card--card-foreground--accent--muted-foreground--border--status-warn--direction-up--direction-down
Pricelive · 0.02 stepsSize
  • 104.121,240
  • 104.10820
  • 104.081,580
  • 104.06670

spread 0.02

  • 104.04720
  • 104.021,010
  • 104.0018,400
  • 103.98610

Depth

104.05

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.

Examples

Captures whose hotspots reference this pattern, grouped by product and dated. The dashed boxes are this pattern; hover any box for the note.

Kraken Pro

The full order book and depth ladder are public with no account, which makes it the only display here updating several times a second that anyone can go and watch.

Trade / BTC-USD September 11, 2026 Kraken Pro, signed out (public market data) dense · dark · desktop-web
The red-green-direction entry argues that the convention should never carry the information alone, and this ladder shows the redundancy arriving for free. Asks are red and bids are green, but asks are also above the spread and bids below it, so the side is encoded twice—once in a hue that a colourblind reader may not separate, and once in a position that everyone can. Convert this panel to greyscale and it still works. The spread row between them is the other good decision: it reads "0.1 (0.0001%)", the absolute and the ratio side by side, so the number means something whether you are trading one Bitcoin or a hundred. Two details worth noticing. The quantity column runs to eight decimal places, which is correct for the asset and unreadable at a glance, and the depth bars behind each row are doing most of the actual communicating. And the "0.10" control at the top of the panel is the price increment the book is aggregated into—change it and the number of levels changes underneath you.
  • Order book and depth Two ladders sharing a price axis, with the spread labelled between them and a depth bar behind every row.
  • Red/green direction coloring Asks in red, and also above the spread. Position carries the side on its own, so greyscale survives.
  • Ratio and rate The spread as 0.1 and as 0.0001% together, which is the absolute and the normalised form on one line.
  • Delta indicator −197.0 USD and −0.26% together, over a named 24-hour base. All three of the decisions, made and stated.
  • Time series Candles rather than a line, so each interval carries open, high, low and close instead of one sampled value.
  • Order book and depth The price increment the book is aggregated into. Change it and the level count changes under the eye.