Documentation

UI components.

Order book, depth, candles, trade tape, venue ticker and a headless order ticket for a Clobber market. Plain DOM: no framework, no build step, no bundler. React wrappers ship in the same package and draw the same widgets.

Everything on this page is live

The components below are running against a simulated venue that speaks the published feed protocol into the published client: snapshot, absolute size deltas, one sequence per market shared across book and trades. It is deterministic, on a fixed seed, so every reader sees the same market. The controls under Loss and resync make it misbehave, so the recovery paths are visible rather than promised.

A page with no build step
<link rel="stylesheet" href="https://unpkg.com/@clobber/ui/dist/clobber-ui.css">
<script src="https://unpkg.com/@clobber/ui"></script>
<div id="market"></div>
<script>
  ClobberUI.mountMarket({
    target: "#market",
    url: "wss://feed-sandbox.clobberhq.com",
    credential: { publishableKey: "pk_live_..." },
    market: "WILL-IT-RAIN-BA",
  });
</script>
npm install @clobber/ui

import { mountOrderBook } from '@clobber/ui/vanilla';
import '@clobber/ui/clobber-ui.css';

const book = mountOrderBook({ target: '#book', depth: 12 });
feed.onBook = (market, state) => book.update(state);

@clobber/ui is on npm, Apache 2.0: one bundle for a page that has no build step, ES modules and types for a page that does, and the stylesheet either way. What it needs from you is a publishable key, which the sandbox hands out with your environment.

Every component is the same four things: update(state) for new data, setStatus(status) for the connection light, retheme() after a skin change, and destroy() to leave the page as it was. That is the entire surface.

MountReactWhat it draws
mountOrderBook<OrderBook>the ladder, cumulative totals, click a level for its price
mountDepthChart<DepthChart>cumulative depth as a shape, both sides meeting at the spread
mountCandleChart<MarketChart>candles and volume, the forming bar updating in place
mountTradeTape<TradeTape>public trades, coloured by the side that took liquidity
mountMarketSummary<MarketSummary>last, the move, the touch, 24h volume, lifecycle badge
mountTickerBanner<TickerBanner>the venue strip, every market of an environment
mountOrderTicket<OrderTicket>the ticket, collateral priced per market kind
mountMarketthe hooksthe whole page on one connection

Four skins, one structure

A skin is a set of CSS variables and nothing else. The markup, the classes and the behaviour are identical in all four, which is why writing a fifth takes twenty lines. Set data-clobber-skin on any ancestor and everything under it follows, canvases included: the depth and candle components read the same variables and repaint.

skin applies to every component on this page

Order book

The ladder, with cumulative totals summed in decimal rather than in floats, a wash behind each row for relative depth, and the spread and mid between the sides. Rows are pooled and mutated in place, so a book updating ten times a second does not repaint the column a trader is reading. Click a level and the price goes wherever you send it: on this page, into the ticket.

const book = ClobberUI.mountOrderBook({
  target: "#book",
  depth: 12,
  onPrice: (level) => ticket.setPrice(level.price),
});

book.update(state);          // the book from the feed
book.setStatus("live");

Depth

The same book as a shape: cumulative size against price, the two sides meeting at the spread. Drawn on a canvas with no charting dependency, because a step area and two axes are a hundred lines and a library here would have to be themed through its own options object instead of through the skin. Hover for the level under the cursor.

const depth = ClobberUI.mountDepthChart({
  target: "#depth",
  depth: 50,     // levels per side folded into the curve
  band: 0.25,    // optional: clamp the axis around the mid
});

depth.update(state);

Candles

Candles and volume on TradingView's lightweight-charts. The forming bucket arrives once a second on candles.{interval} and is written with a single series update, so the last bar grows in place instead of the series being replaced.

History is yours to fetch

The feed publishes the forming bucket and nothing older, and a publishable key opens the feed and nothing else. Past candles come from GET /v1/markets/{symbol}/candles through your own backend and reach the chart through the history callback. On this page that callback is answered by the simulation.

const chart = ClobberUI.mountCandleChart({
  target: "#chart",
  intervals: ["1m", "5m", "1h", "1d"],
  interval: "1m",
  onInterval: (iv) => resubscribe(iv),
});

chart.update(candles);   // history first, then the forming bucket

Trades

The public tape, newest first, coloured by the side that took liquidity. It is handed the whole list rather than one trade at a time, because the list is what a feed client already holds and a component with its own history would drift from it after a resync.

const tape = ClobberUI.mountTradeTape({
  target: "#tape",
  rows: 40,
  compact: false,   // true abbreviates 12400 as 12.4K
});

tape.update(trades);   // newest first

Summary

Last, the move, the touch, the day's volume, and the lifecycle state told honestly: a halted, closed, resolved or voided market says so in a badge instead of rendering as a live market with stale numbers. Press halt the market under Loss and resync and watch it. The change is measured from a reference price you supply, and the label always names what it is measured from.

const summary = ClobberUI.mountMarketSummary({
  target: "#summary",
  market: "WILL-IT-RAIN-BA",
  referenceLabel: "24h",
});

summary.update({ ticker, event, reference: "0.58" });

Order ticket

The ticket refuses two things. It refuses a credential: there is no key option, no header option and no base URL, and placing an order goes through an adapter function you supply, which runs on your backend with your key. An API key in a browser is every account on your platform, and the only way to make that impossible rather than discouraged is to give the types nowhere to put one.

It also refuses to invent a number. The collateral line appears when you tell the ticket what kind of market this is, because a binary short posts (1 - price) x qty, a scalar short posts (max - price) x qty, and a pair seller posts the asset itself. Tick and lot are checked before the round trip. All of it is computed in scaled integers.

ClobberUI.mountOrderTicket({
  target: "#ticket",
  market: {
    symbol: "WILL-IT-RAIN-BA",
    kind: "binary",
    tickSize: "0.01",
    lotSize: "1",
    settlementCurrency: "USDC",
  },
  submit: async (order) => {
    const r = await fetch("/api/orders", {
      method: "POST",
      body: JSON.stringify(order),
    });
    return r.ok ? { ok: true } : { ok: false, error: await r.text() };
  },
});

A market page

One call mounts summary, chart, book, depth, tape and ticket over a single connection. One connection matters: six components on six sockets would be six snapshots, six sequence chains and six resyncs out of step with each other. Painting is coalesced into one animation frame, so a burst of deltas costs one layout.

ClobberUI.mountMarket({
  target: "#market",
  url: "wss://feed-sandbox.clobberhq.com",
  credential: { publishableKey: "pk_live_..." },
  market: { symbol: "SOL-USDC", kind: "pair", tickSize: "0.01", settlementCurrency: "USDC" },
  interval: "1m",
  history: (iv) => fetch(`/api/candles?interval=${iv}`).then((r) => r.json()),
  submit: (order) => fetch("/api/orders", { method: "POST", body: JSON.stringify(order) })
    .then((r) => (r.ok ? { ok: true } : { ok: false, error: "rejected" })),
});

React

The React components mount the same components and draw nothing of their own. Two renderers for one component is two sets of bugs, and the promise of the package is that a React page and a script tag see the same book, the same colours and the same recovery behaviour. Every example on this page has a react tab: that is the whole API.

What is React's own is the hooks. One subscription per hook call, torn down with the component, and the per slice hooks are views of the same connection rather than new ones.

HookGives you
useMarketFeedstatus, book, trades, ticker, candles and the lifecycle event for one market
useOrderBookstatus and the book
useTradesstatus and the tape
useCandlesstatus and the interval's candles, the forming one live
useMarketstatus, ticker and the lifecycle event
useVenueFeedstatus and a row per market of the environment, for the ticker
The credential never changes the connection

A getToken callback that is a new closure on every render does not reconnect the feed: the hooks hold it behind a ref, so a fifteen minute token can be minted again on every reconnect without the component churning.

Loss and resync

Book and trade frames carry seq and prev. A frame whose prev is not the last sequence delivered for that market means something was lost, and the recovery is to subscribe again: the snapshot that answers re-anchors the chain at its own sequence. Derived channels (ticker, candles) carry no chain and are never gap checked, because a candle has no sequence of its own. All of that is implemented once, in the client every component here stands on, and it is the sequencing rule from the reference, not a second interpretation of it.

These three drive it against every component on the page. Force a gap skips a sequence, and the status light pulses while the client re-anchors. Drop the connection closes the socket underneath, and the client reconnects on its own backoff and subscribes again. Neither one needs anything from your code.

speed

Theming

Declare the variables on any ancestor. Undeclared ones fall back to the dashboard skin, so a host can override three tokens and leave the rest.

A skin of your own
.my-desk {
  --clobber-font: "Inter", system-ui, sans-serif;
  --clobber-mono: "Roboto Mono", monospace;
  --clobber-bg: #0b0f14;
  --clobber-panel: #121820;
  --clobber-sunken: #0b0f14;
  --clobber-fg: #e7edf5;
  --clobber-muted: #7d8b9c;
  --clobber-grid: #223040;
  --clobber-grid-soft: #18222e;
  --clobber-bid: #2bb673;
  --clobber-ask: #e05561;
  --clobber-bid-wash: rgba(43,182,115,.14);
  --clobber-ask-wash: rgba(224,85,97,.14);
  --clobber-accent: #2bb673;
  --clobber-accent-fg: #08131d;
  --clobber-badge: #f0bb48;
  --clobber-radius: 3px;
  --clobber-pad: 10px;
  --clobber-row: 20px;
}

Canvases cannot inherit a CSS variable the way markup does, so the depth and candle components read the values and repaint. A skin change anywhere above them is observed; a host that swaps a stylesheet wholesale calls retheme().