SolanaUISolanaUI

Order Book

A vertical orderbook displaying bids and asks with cumulative depth

PriceSizeTotal
162.8940K402K
162.8618K362K
162.8345K344K
162.8023K299K
162.7736K276K
162.7448K240K
162.7128K193K
162.6856K164K
162.6520K109K
162.6242K89K
162.5931K47K
162.5716K16K
162.56Spread0.02 (0.012%)
162.5512K12K
162.5328K41K
162.5045K86K
162.4819K105K
162.4563K167K
162.4234K201K
162.4052K253K
162.3822K275K
162.3542K317K
162.3259K376K
162.2915K391K
162.2637K428K
import { OrderBook } from "@/components/sol/order-book";

export function OrderBookDemo() {
  const bids = [
    { price: 162.55, size: 12400 },
    { price: 162.53, size: 28100 },
    { price: 162.50, size: 45300 },
    { price: 162.48, size: 18700 },
    { price: 162.45, size: 62500 },
  ];

  const asks = [
    { price: 162.57, size: 15800 },
    { price: 162.59, size: 31200 },
    { price: 162.62, size: 42100 },
    { price: 162.65, size: 19500 },
    { price: 162.68, size: 55700 },
  ];

  return <OrderBook bids={bids} asks={asks} />;
}

Installation

pnpm dlx shadcn@latest add @solanaui/order-book
npx shadcn@latest add @solanaui/order-book
yarn dlx shadcn@latest add @solanaui/order-book

Usage

<OrderBook
  bids={[
    { price: 162.55, size: 12400 },
    { price: 162.53, size: 28100 },
    { price: 162.50, size: 45300 },
  ]}
  asks={[
    { price: 162.57, size: 15800 },
    { price: 162.59, size: 31200 },
    { price: 162.62, size: 42100 },
  ]}
/>

Source Code

import { cn } from "@/lib/utils";interface OrderBookProps {  bids: {    price: number;    size: number;  }[];  asks: {    price: number;    size: number;  }[];  className?: string;}const formatSize = (value: number) => {  if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`;  if (value >= 1_000) return `${(value / 1_000).toFixed(0)}K`;  return value.toFixed(0);};const OrderBook = ({ bids, asks, className }: OrderBookProps) => {  if (bids.length === 0 && asks.length === 0) return null;  const highestBid = bids[0]?.price ?? 0;  const lowestAsk = asks[0]?.price ?? 0;  const midPrice =    bids.length > 0 && asks.length > 0      ? (highestBid + lowestAsk) / 2      : highestBid || lowestAsk;  const spread = lowestAsk - highestBid;  const spreadPercent = highestBid > 0 ? (spread / highestBid) * 100 : 0;  let cumulativeBidSize = 0;  const bidTotals = bids.map((bid) => {    cumulativeBidSize += bid.size;    return cumulativeBidSize;  });  let cumulativeAskSize = 0;  const askTotals = asks.map((ask) => {    cumulativeAskSize += ask.size;    return cumulativeAskSize;  });  const maxTotal = Math.max(    bidTotals[bidTotals.length - 1] || 0,    askTotals[askTotals.length - 1] || 0,  );  const reversedAsks = [...asks].reverse();  const reversedAskTotals = [...askTotals].reverse();  return (    <div      className={cn(        "w-full flex flex-col overflow-hidden rounded-lg border bg-card text-card-foreground",        className,      )}    >      {/* Column headers */}      <div className="grid grid-cols-3 px-3 py-2 text-xs text-muted-foreground border-b">        <span>Price</span>        <span className="text-right">Size</span>        <span className="text-right">Total</span>      </div>      {/* Asks (reversed so lowest ask is at bottom, near spread) */}      <div className="flex flex-col overflow-auto">        {reversedAsks.map((ask, i) => {          const total = reversedAskTotals[i];          const depth = maxTotal > 0 ? (total / maxTotal) * 100 : 0;          return (            <div key={`ask-${ask.price}`} className="relative">              <div                className="absolute right-0 top-0 bottom-0 bg-red-400/10"                style={{ width: `${depth}%` }}              />              <div className="relative grid grid-cols-3 px-3 py-1 text-xs">                <span className="text-red-400 font-medium tabular-nums">                  {ask.price.toFixed(2)}                </span>                <span className="text-right text-muted-foreground tabular-nums">                  {formatSize(ask.size)}                </span>                <span className="text-right text-muted-foreground tabular-nums">                  {formatSize(total)}                </span>              </div>            </div>          );        })}      </div>      {/* Spread / mid price */}      <div className="grid grid-cols-3 px-3 py-2 border-y">        <span className="text-lg font-semibold text-emerald-500 tabular-nums">          {midPrice.toFixed(2)}        </span>        <span className="text-right text-xs text-muted-foreground self-center">          Spread        </span>        <span className="text-right text-xs text-muted-foreground self-center tabular-nums">          {spread.toFixed(2)} ({spreadPercent.toFixed(3)}%)        </span>      </div>      {/* Bids */}      <div className="flex flex-col overflow-auto">        {bids.map((bid, i) => {          const total = bidTotals[i];          const depth = maxTotal > 0 ? (total / maxTotal) * 100 : 0;          return (            <div key={`bid-${bid.price}`} className="relative">              <div                className="absolute right-0 top-0 bottom-0 bg-emerald-500/10"                style={{ width: `${depth}%` }}              />              <div className="relative grid grid-cols-3 px-3 py-1 text-xs">                <span className="text-emerald-500 font-medium tabular-nums">                  {bid.price.toFixed(2)}                </span>                <span className="text-right text-muted-foreground tabular-nums">                  {formatSize(bid.size)}                </span>                <span className="text-right text-muted-foreground tabular-nums">                  {formatSize(total)}                </span>              </div>            </div>          );        })}      </div>    </div>  );};export type { OrderBookProps };export { OrderBook };