Order Book
Level II bid/ask depth ladder with cumulative size bars and spread readout. Install Order Book from the VLLNT UI registry with the shadcn CLI.
Level II bid/ask depth ladder with cumulative size bars and spread readout. Part of the data family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Order Book to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/order-book.jsonSource
import * as React from "react";
import type { HeadingTag } from "../../lib/types";
import { cn } from "../../lib/utils";
export type OrderBookLevel = {
price: number;
size: number;
total?: number;
};
export type OrderBookProps = {
/** Heading tag for the main "Order book" title. Defaults to `h2`. */
as?: HeadingTag;
asks: OrderBookLevel[];
bids: OrderBookLevel[];
precision?: number;
} & React.HTMLAttributes<HTMLDivElement>;
function withCumulativeTotal(levels: OrderBookLevel[]) {
let runningTotal = 0;
return levels.map((level) => {
runningTotal += level.total ?? level.size;
return {
...level,
total: level.total ?? runningTotal,
};
});
}
function formatNumber(value: number, precision = 2) {
return value.toLocaleString(undefined, {
maximumFractionDigits: precision,
minimumFractionDigits: precision,
});
}
function BookSide({
accent,
levels,
precision,
title,
}: {
accent: "ask" | "bid";
levels: OrderBookLevel[];
precision: number;
title: string;
}) {
const maxTotal = Math.max(...levels.map((level) => level.total ?? 0), 1);
const barClassName =
accent === "ask"
? "bg-rose-500/12 border-rose-500/15"
: "bg-emerald-500/12 border-emerald-500/15";
const priceClassName =
accent === "ask"
? "text-rose-600 dark:text-rose-400"
: "text-emerald-600 dark:text-emerald-400";
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold uppercase tracking-[0.24em] text-muted-foreground">
{title}
</h3>
<span className="text-xs text-muted-foreground">
Depth by total size
</span>
</div>
<div className="rounded-2xl border border-border/70 bg-background/60 p-2">
<div className="grid grid-cols-[1.2fr_1fr_1fr] gap-3 px-3 py-2 text-[11px] font-medium uppercase tracking-[0.2em] text-muted-foreground">
<span>Price</span>
<span className="text-right">Size</span>
<span className="text-right">Total</span>
</div>
<div className="space-y-1">
{levels.map((level) => {
const width = `${((level.total ?? 0) / maxTotal) * 100}%`;
return (
<div
className="relative overflow-hidden rounded-xl border border-transparent px-3 py-2"
key={`${accent}-${level.price}-${level.size}`}
>
<div
className={cn(
"absolute inset-y-0 right-0 rounded-xl border",
barClassName,
)}
style={{ width }}
/>
<div className="relative grid grid-cols-[1.2fr_1fr_1fr] gap-3 text-sm tabular-nums">
<span className={cn("font-medium", priceClassName)}>
{formatNumber(level.price, precision)}
</span>
<span className="text-right text-foreground">
{formatNumber(level.size, 3)}
</span>
<span className="text-right text-muted-foreground">
{formatNumber(level.total ?? 0, 3)}
</span>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}
export const OrderBook = ({
as: Heading = "h2",
asks,
bids,
className,
precision = 2,
ref: reference,
...props
}: OrderBookProps & { ref?: React.Ref<HTMLDivElement> }) => {
if (asks.length === 0 && bids.length === 0) {
return null;
}
const askLevels = withCumulativeTotal(asks);
const bidLevels = withCumulativeTotal(bids);
const bestAsk = askLevels[0];
const bestBid = bidLevels[0];
const spread =
bestAsk && bestBid ? Math.max(bestAsk.price - bestBid.price, 0) : 0;
return (
<div
className={cn(
"rounded-2xl border border-border bg-card/80 p-4 shadow-sm",
className,
)}
ref={reference}
{...props}
>
<div className="mb-4 flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-xs font-medium uppercase tracking-[0.28em] text-muted-foreground">
Level II
</p>
<Heading className="text-lg font-semibold text-foreground">
Order book
</Heading>
</div>
<div className="rounded-full border border-border bg-background/70 px-3 py-1 text-sm text-muted-foreground tabular-nums">
Spread {formatNumber(spread, precision)}
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<BookSide
accent="ask"
levels={askLevels}
precision={precision}
title="Asks"
/>
<BookSide
accent="bid"
levels={bidLevels}
precision={precision}
title="Bids"
/>
</div>
</div>
);
};
OrderBook.displayName = "OrderBook";
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.