Trading UI development has a reputation for being harder than it looks, and the reputation is earned. A trading UI is not one screen. It is a dozen small real-time applications sharing a window, each with its own data feed, its own formatting rules, and its own way of failing. Teams that treat it like a dashboard with a few extra widgets usually discover the difference around week six, when the order book stutters, the totals are off by a fraction of a cent, and nobody can explain why the layout resets itself on refresh.
Most of that pain comes from sequencing rather than difficulty. Build the panels before the data layer and you rewrite every panel. Build order entry before you have settled precision and you ship rounding bugs. This post lays out the order that works, stage by stage, with the specific traps that sit at each one. It is the sequence the Hedge UI demo followed, and where the demo's git history shows a correction, I have called it out.
The seven stages at a glance
| Stage | What you build | The trap |
|---|---|---|
| 1. Foundations | Build tooling, strict TypeScript, design primitives | Picking a meta-framework you do not need |
| 2. Market data layer | REST reference data, websocket streams, normalisation | Letting every component open its own socket |
| 3. Core market panels | Order book, trades feed, price chart | Rendering every message |
| 4. Order entry | The order form, validation, precision | Floating point arithmetic |
| 5. Account views | Balances, open orders, history tables | A heavyweight data grid |
| 6. Workspace | Dockable layout, saved layouts, persistence | Unversioned persisted state |
| 7. Hardening | Tests, error states, accessibility, performance budgets | Leaving it all until launch week |
The stages overlap in practice, but the dependencies between them are real. Each one assumes the stage before it is stable.
Stage 1: Foundations
A trading terminal is a long-lived, client-rendered, single-screen application. There are no routes worth server rendering and no content for a crawler, so the foundation is a plain SPA: a bundler, a strict compiler, and a set of UI primitives.
The demo runs on Vite 7 with the SWC React plugin, React 19, and TypeScript 5.8 in strict mode with noUnusedLocals and noUnusedParameters switched on. The reasoning for an SPA over a server-rendered framework is laid out in the write-up on build tooling for a trading SPA, and it has not changed: server rendering buys a terminal nothing and adds a Node process to the hot path.
Two decisions at this stage are cheap now and expensive later:
- Turn strict mode on from the first commit. Financial data is full of optional fields, string-encoded decimals, and unions of order states. Retro-fitting strictness onto 8,000 lines is a week of work. The patterns that pay off most are in the piece on TypeScript patterns for financial data.
- Choose primitives you own. The demo uses shadcn/ui components copied into
src/components/ui/on top of Radix. Because the source lives in the repo, addingbuyandsellbutton variants was a two-line change tobutton-variants.tsrather than a fight with a theme override API.
The second commit in the demo's history is "Add tailwind + shadcn", one day after the initial commit. Styling primitives went in before any feature did.
Stage 2: The market data layer
This is the stage teams skip, and it is the one that determines whether everything after it is pleasant or painful. Before a single panel exists, you want one module that owns the connection to the exchange and exposes typed hooks to the rest of the app.
There are two halves. Reference data (which symbols exist, their tick sizes, their step sizes) arrives over REST and changes rarely. Live data (depth, trades, tickers) arrives over websocket and never stops. They deserve different tools. In the demo, src/binance/binance-rest.ts wraps REST calls in TanStack Query hooks such as useBinanceExchangeInfo, and src/binance/binance-stream.ts wraps the stream API with react-use-websocket. A third file, binance-mapper.ts, converts exchange DTOs into the app's own Product type so that no panel ever sees a raw Binance field name.
That mapper is small but it carries a lot of weight. It reads the PRICE_FILTER and LOT_SIZE filters from the exchange info response and turns tickSize and stepSize into display precisions:
export const toProduct = (symbol: ExchangeInfoSymbol): Product => { const tickSize = getTickSize(symbol); const stepSize = getStepSize(symbol); return { id: symbol.symbol, symbol: symbol.baseAsset + "/" + symbol.quoteAsset, quote: { code: symbol.quoteAsset, precision: getPrecision(Number(tickSize)), tickSize, }, base: { code: symbol.baseAsset, precision: getPrecision(Number(stepSize)), stepSize, }, }; };
Every panel built later formats prices with product.quote.precision and quantities with product.base.precision. Because that decision was made once, at the data boundary, the order book, the trades feed, the order form, and the tables all agree on how many decimals BTC/USDT has.
The trap at this stage is the per-component socket. It is the natural thing to write: a useEffect that opens a WebSocket, subscribes, and closes on unmount. It works for one panel and collapses at nine. The patterns for avoiding it, including subscription diffing over a shared connection, are covered in websocket state management for crypto trading apps. The split between server state and client state is covered in state management with Context and React Query.
Stage 3: Core market panels
With a data layer in place, the three panels that define a trading UI can be built in roughly this order: order book, trades feed, chart.
The order book first, because it stresses everything. It is the highest-frequency panel, it needs precise formatting, and it has the most demanding layout. If your data layer and rendering approach survive the order book, they will survive the rest. The full walkthrough is in the real-time order book deep dive.
The trades feed second. It looks trivial, a list of prints, and it is where the first performance bug usually appears. The demo's history has a commit on 16 July 2025 titled "Fix market trade too many updates", two weeks into the project. A liquid symbol emits trades far faster than a screen can usefully show them, and rendering each one individually pins the main thread. The fix was to batch, and the general techniques are in the post on throttling and batching market data.
The chart third, and here the main decision is build versus embed. A candlestick chart with indicators and drawing tools is months of work. The comparison of the options is in choosing a charting library. Our own choice was to embed the TradingView widget, which arrived in the demo on 1 September 2025, a full two months after the first commit. The chart is the panel users look at most and it was one of the last things added, because it depends on nothing and nothing depends on it.
The trap in this stage is rendering every message. The screen refreshes 60 times a second and a human reads perhaps five updates a second. Anything between the socket and the DOM that does not drop, batch, or coalesce is wasted work. The framework for thinking about it is in latency budgets for trading UIs.
Stage 4: Order entry
Order entry comes after market data for a practical reason: the form needs the product's tick size, step size, and a live price to be useful, and those come from stages 2 and 3.
The rule for this stage is short. No floating point arithmetic touches a price, a quantity, or a total. 0.1 + 0.2 is not 0.3 in JavaScript, and an order form that shows a total one unit off in the eighth decimal place destroys trust faster than any outage. The demo's OrderFormProvider computes the total with decimal.js:
const result = new Decimal(limitPrice).mul(amount); const fixedValue = fixTotalToPrecision(result.toFixed(precision), precision);
Inputs are kept as strings end to end, truncated to the product's precision as the user types, and only converted for arithmetic. The details, including tick and step size validation, are in decimal precision in trading order forms.
A point worth making about scope: the demo's order form is deliberately a plain limit order ticket. The help text in the panel says so directly, because every institution has different order types and parameters. Build the simple ticket well, with correct arithmetic and clear validation, and extend it once your execution venue's requirements are known. Speculatively building stop-limit, OCO, and iceberg support before you have an API to send them to is how order entry turns into a three-month stage.
Stage 5: Account views
Balances, open orders, and trade history are tables. They are less glamorous than the order book and they are where users spend a surprising amount of time, so sorting, sticky headers, and readable number formatting matter.
The temptation is to install a full data grid. For most trading UIs that is more machinery than the job needs, and it brings its own styling system that fights your theme. A headless table library gives you sorting and row models while you keep control of the markup. The demo's balances, open orders, and favourites panels all sit on one 125-line DataTable component built on TanStack Table, described in headless tables for trading data.
The performance lesson from this stage is in the git log too. On 13 August 2025 there are two consecutive commits: "Memoize open orders" and "Fix performance of open orders by throttling prices". A hundred rows, each showing a live price, re-rendering on every ticker message is a lot of wasted work. The table now reads prices through a useThrottledValue hook with a 1,000ms delay. Nobody reading an open orders table needs sub-second price updates on every row.
Stage 6: The workspace
Only once the panels exist does it make sense to build the thing that arranges them. A professional trading interface lets users drag, dock, resize, and tab panels, and save the result as a named layout.
This stage has two parts. The docking surface itself, which the demo delegates to FlexLayout behind its own serialisation layer, is covered in resizable, dockable panel layouts. The persistence that makes a layout survive a refresh is covered in persisting UI state to localStorage.
The trap is unversioned state. The moment you persist a layout tree to a user's browser, you have a schema you cannot migrate by deploying. The demo's history shows the lesson being learned: "Add version prefix to storage" landed on 13 August 2025, after panel state storage had already shipped on 31 July. Every storage key is now prefixed with a version constant, and a breaking change to the shape means bumping it rather than writing migration code. Put the prefix in on day one.
The connective tissue between workspace and panels is a registry: a map from a panel type ID to a component. It is what lets the layout engine render panels it knows nothing about, and it is described in the post on feature folders and the panel factory.
Stage 7: Hardening
Everything in this stage can be started earlier, and should be. It is listed last because this is when it becomes unavoidable.
- Tests. Real-time components are testable if the presentational layer is separated from the data layer. Every feature in the demo has a pure component that takes props and a panel wrapper that feeds it, and the tests target the pure component in a real browser. See testing real-time React components.
- Failure states. Feeds drop, REST calls fail, and a single panel can throw. The patterns are in connection status and loading states and error boundaries and graceful degradation.
- Accessibility. Keyboard access and colour-independent signals are functional requirements for traders, as argued in accessibility in trading UIs.
- Security. Token storage and content security policy need a decision before real accounts are connected. The checklist is in security for browser-based trading applications.
I will be candid about our own sequencing here. The demo's test suite and CI checks landed in December 2025, five months after the first commit. The features were stable by then and the tests were straightforward to write because of the panel and presentational split, but it would have been cheaper to add them alongside each feature. If you are starting today, wire the test runner up in stage 1 and write the first test with the first panel.
How long does it take
Teams that have not built one before tend to estimate three to four months. For two or three frontend engineers building all seven stages to production quality, eight to twelve months is closer, and the distribution is not what people expect. The core panels in stage 3 feel like the bulk of the work and are perhaps a quarter of it. The data layer, the workspace, and hardening together take more than half. The cost breakdown, and when it makes sense to start from an existing foundation instead, is in build versus buy for crypto exchanges. The failure modes of teams that underestimate it are in what most trading UI projects get wrong.
Closing guidance
If you take one thing from this roadmap, take the ordering. Data layer before panels. Precision before order entry. Panels before workspace. Version prefix before persistence. Each of those is a place where doing it in the other order means doing it twice.
The second thing is to keep each stage boring. A trading UI earns its complexity from the domain: precision, frequency, and failure. It does not need more from the architecture. One socket abstraction, one table component, one registry, one storage hook. The Hedge UI demo is nine panels in roughly 8,500 lines, and most of the effort went into making each of those lines unsurprising.
