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.jsonNamespace (after one-time setup)
npx shadcn@latest add @livedocs/order-bookUsage
tsx
import { OrderBook } from "@/components/ui/order-book"
export function Example() {
return <OrderBook symbol="BTC/USDT" basePrice={97500} />
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| symbol | string | — | Trading pair label |
| basePrice | number | — | Mid price for the simulated book |
| tickSize | number | — | Price step between levels |
| rows | number | — | Levels per side |
| seed | number | — | Book randomness seed |
| tickMs | number | — | Milliseconds between book refreshes |
| isLoading | boolean | — | Show shared loading feedback |
| reaction | ChartReactionOptions | — | Optional 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>
);
}