charts
Registry

Order Book

Live bid/ask ladder with depth bars, spread readout, and imbalance meter.

Preview

Preview

Order book

$97,590.000.545413.6156
$97,580.000.282713.0703
$97,570.001.521812.7876
$97,560.000.503311.2658
$97,550.001.112810.7625
$97,540.002.13829.6497
$97,530.005.69417.5115
$97,520.000.72991.8174
$97,510.001.08751.0875

$97,500.00

Spread $20.00

$97,490.001.12291.1229
$97,480.006.16777.2906
$97,470.002.19089.4814
$97,460.000.879910.3613
$97,450.001.913212.2745
$97,440.000.866513.1410
$97,430.000.866214.0072
$97,420.002.248516.2557
$97,410.001.380417.6361
B 56.4%A 43.6%

Loading

Loading
Loading

Installation

Install with the shadcn CLI. This copies source into your project.

bash
npx shadcn@latest add https://livedocs.xyz/r/order-book.json
Namespace (after one-time setup)
npx shadcn@latest add @livedocs/order-book

Usage

tsx
import { OrderBook } from "@/components/ui/order-book"

export function Example() {
  return <OrderBook symbol="BTC/USDT" basePrice={97500} />
}

Props

PropTypeDefaultDescription
symbolstringTrading pair label
basePricenumberMid price for the simulated book
tickSizenumberPrice step between levels
rowsnumberLevels per side
seednumberBook randomness seed
tickMsnumberMilliseconds between book refreshes
isLoadingbooleanShow shared loading feedback
reactionChartReactionOptionsOptional explicit emotion or metric comparison

Source

"use client";

import * as React from "react";

import { Card } from "@/components/ui/card";
import { ChartReaction, ChartSkeleton } from "@/components/ui/chart-reactions";
import { formatPrice, mulberry32, type ChartReactionOptions } from "@/components/ui/crypto-feed";
import { cn } from "@/lib/utils";

export type BookLevel = { price: number; amount: number; total: number };

function buildBook(seed: number, mid: number, rows: number, tick: number): { asks: BookLevel[]; bids: BookLevel[] } {
  const rand = mulberry32(seed);
  const asks: BookLevel[] = [];
  const bids: BookLevel[] = [];
  let askTotal = 0;
  let bidTotal = 0;
  for (let index = 1; index <= rows; index++) {
    const askAmount = 0.05 + rand() * 2.4 + (index === 3 ? 4.5 : 0);
    const bidAmount = 0.05 + rand() * 2.4 + (index === 2 ? 5.2 : 0);
    askTotal += askAmount;
    bidTotal += bidAmount;
    asks.push({ price: mid + index * tick, amount: askAmount, total: askTotal });
    bids.push({ price: mid - index * tick, amount: bidAmount, total: bidTotal });
  }
  return { asks: [...asks].reverse(), bids };
}

function Row({ level, max, side, index }: { level: BookLevel; max: number; side: "bid" | "ask"; index: number }) {
  return (
    <div
      role="row"
      aria-label={`${side === "bid" ? "Bid" : "Ask"} ${level.price} amount ${level.amount.toFixed(4)}`}
      className="relative grid grid-cols-[1fr_1fr_1fr] px-2 py-[3px] font-mono text-[11px] tabular-nums"
    >
      <span
        aria-hidden
        className={cn("absolute inset-y-[1px] right-0 rounded-sm", side === "bid" ? "bg-chart-up/15" : "bg-destructive/15")}
        style={{ width: `${Math.max(2, (level.total / max) * 100)}%` }}
      />
      <span className={cn("relative", side === "bid" ? "text-chart-up" : "text-destructive")}>${formatPrice(level.price)}</span>
      <span className="relative text-right text-foreground">{level.amount.toFixed(4)}</span>
      <span className="relative text-right text-muted-foreground">{level.total.toFixed(4)}</span>
    </div>
  );
}

export function OrderBook({
  symbol = "BTC/USDT",
  basePrice = 97500,
  tickSize = 10,
  rows = 9,
  seed = 21,
  tickMs = 1500,
  className,
  isLoading,
  reaction,
}: {
  symbol?: string;
  basePrice?: number;
  tickSize?: number;
  rows?: number;
  seed?: number;
  tickMs?: number;
  className?: string;
  isLoading?: boolean;
  reaction?: ChartReactionOptions;
}) {
  const [nonce, setNonce] = React.useState(0);
  const book = React.useMemo(() => buildBook(seed + nonce * 101, basePrice, rows, tickSize), [seed, nonce, basePrice, rows, tickSize]);
  const drift = React.useRef(mulberry32(seed));

  React.useEffect(() => {
    const id = window.setInterval(() => {
      if (document.hidden) return;
      drift.current();
      setNonce((value) => value + 1);
    }, tickMs);
    return () => window.clearInterval(id);
  }, [tickMs]);

  const bestAsk = book.asks[book.asks.length - 1]!;
  const bestBid = book.bids[0]!;
  const spread = bestAsk.price - bestBid.price;
  const max = Math.max(...book.asks.map((level) => level.total), ...book.bids.map((level) => level.total));
  const bidVolume = book.bids.reduce((sum, level) => sum + level.amount, 0);
  const askVolume = book.asks.reduce((sum, level) => sum + level.amount, 0);
  const bidShare = (bidVolume / (bidVolume + askVolume)) * 100;

  void reaction;

  return (
    <Card className={cn("min-w-0 w-full p-5", className)} role="region" aria-label={`${symbol} order book`}>
      <ChartSkeleton isLoading={isLoading}>
        <div className="flex items-center justify-between gap-2">
          <p className="truncate text-[15px] font-medium tracking-tight">Order book</p>
          {!isLoading ? <ChartReaction reaction={reaction} /> : <p className="font-mono text-[11px] text-muted-foreground">{symbol}</p>}
        </div>
        <div className="mt-1 grid grid-cols-[1fr_1fr_1fr] px-2 font-mono text-[10px] uppercase tracking-wide text-muted-foreground" aria-hidden>
          <span>Price</span><span className="text-right">Amount</span><span className="text-right">Total</span>
        </div>
        <div role="table" aria-label="Asks" className="mt-1">
          {book.asks.map((level, index) => (
            <Row key={`ask-${level.price}`} level={level} max={max} side="ask" index={index} />
          ))}
        </div>
        <div className="flex items-center gap-2 border-y border-border px-2 py-1.5" role="status">
          <p className="font-mono text-sm font-semibold tabular-nums text-foreground">${formatPrice((bestAsk.price + bestBid.price) / 2)}</p>
          <p className="font-mono text-[11px] tabular-nums text-muted-foreground">Spread ${formatPrice(spread)}</p>
        </div>
        <div role="table" aria-label="Bids">
          {book.bids.map((level, index) => (
            <Row key={`bid-${level.price}`} level={level} max={max} side="bid" index={index} />
          ))}
        </div>
        <div className="mt-3">
          <div className="flex h-1.5 overflow-hidden rounded-full bg-destructive/25" role="img" aria-label={`Bid ${bidShare.toFixed(0)} percent, ask ${(100 - bidShare).toFixed(0)} percent`}>
            <span className="h-full bg-chart-up" style={{ width: `${bidShare}%` }} />
          </div>
          <div className="mt-1 flex justify-between font-mono text-[10px] tabular-nums text-muted-foreground">
            <span className="text-chart-up">B {bidShare.toFixed(1)}%</span>
            <span className="text-destructive">A {(100 - bidShare).toFixed(1)}%</span>
          </div>
        </div>
      </ChartSkeleton>
    </Card>
  );
}