Candlestick Chart
Token-driven candlestick (OHLC) chart built on Recharts v3 — one candle per period, hollow when it closed up and filled when it closed down, with optional volume bars, a range selector and a readout hook.
pnpm add @tessinaui/uiWhen to use
- To show how a price moved within each period. A candle carries four numbers a line cannot: where the period opened, its high, its low and where it closed.
- To show the size of each day's move as well as the trend. Long bodies are decisive days, long wicks are days that tried and came back.
- Beside volume. Set
volumeon the rows and the bottom of the plot shows how much traded each period (Volume).
When not to use
- For the price trend alone. If only the close matters, use LineChart; it reads faster and fits a smaller space (In a card switches between the two).
- For anything that is not a price. Open, high, low and close only mean something for a value that trades through a period.
- For more than about a hundred candles on screen. Bodies shrink to their wicks. Show a shorter period, or add the range selector (Range selector).
- For a trading terminal. Drawing tools, indicator panes, depth and order books are a different product; this chart draws prices.
Installation
pnpm add @tessinaui/ui rechartsUsage
import { CandlestickChart } from "@tessinaui/ui";
// One row per period, oldest first.
const data = [
{ date: "2026-03-02", open: 184.26, high: 187.48, low: 184.24, close: 186.37 },
{ date: "2026-03-03", open: 186.6, high: 187.03, low: 185.74, close: 185.97 },
{ date: "2026-03-04", open: 186.72, high: 187.43, low: 184.1, close: 185.11 },
];
<CandlestickChart
data={data}
valueFormatter={(value) => usd.format(value)}
label="Northwind shares slipped for three days"
/>Each row is one period. date can be a date string, a Date or epoch
milliseconds; the four prices are numbers. A row missing any price is a period
with no trading. There is no config: up and down take the status inks.
Examples
Default
Hover a candle, or Tab to the chart and use the arrow keys: the candle is read in the tooltip and the others step back.
Volume
Add volume to the rows and the bottom fifth of the plot shows it, one bar per
candle in its candle's direction. showVolume={false} hides it.
Range selector
showBrush draws a range selector under the chart. The price axis fits the
candles in range, so a quarter fills the plot. defaultBrushRange sets the
first range and onBrushChange reports a new one, both as row indices.
Readout
onActiveIndexChange reports the row under the pointer or the keyboard, so the
day's prices can sit above the chart instead of in a tooltip. With
showTooltip={false} the readout is the live region, so give it
role="status".
Last price
A dashed ReferenceLine child at the last close.
Moving average
Compute the average into the rows and compose a Line child that reads it by
key. The chart draws the line it is given.
In a card
The price, the change, the range and the chart type belong to the card. The chart draws the rows it is given, and switches to a line of the close.
Formatting
valueFormatter formats prices on the axis and in the tooltip,
volumeFormatter the volume, and dateFormatter the tooltip heading and the
ticks. Five-minute candles take a time; give the formatter a fixed time zone so
the server and the browser agree.
Sizes
Three aspect ratios: sm (short and wide), md, and lg (tall). A short plot
holds fewer price ticks.
Accessible name and data table
label states the insight, description carries the numbers, and a table
holds the same rows for anyone who cannot read the candles.
Choosing a day
With selectionMode="single" a click or a tap over a day, or Enter or Space on
the day the arrow keys are on, chooses it; choosing it again clears it. A day's
key is its date. The chosen period keeps the hover band, and its tick becomes the inverse
chip, so the choice reads without colour. Here the readout above the
chart follows the chosen day, and shows the latest day when none is chosen.
On a coloured plate
Inside a coloured Surface the candles take the plate's direction inks and the
chrome follows the plate, with no prop. On plates that cannot carry a hue, up
and down share the plate's ink, and hollow and filled bodies still tell them
apart. The tooltip is a page panel, so it keeps the page's look.
Right-to-left
The chart reads the direction around it. In a right-to-left page time runs from
right to left and the price axis moves to the left-hand side, the end of the
line. Set dir="rtl" and the tooltip's words switch to Arabic as well;
strings overrides them for another language, and yAxisPosition keeps the
price axis on one side.
States
Loading skeleton, empty state, error, and a week with a doji and a market holiday.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
data | { date, open, high, low, close, volume? }[] | — | One row per period, oldest first. Other keys are kept for composed children. |
showVolume | boolean | any row has a volume | Draw volume bars in the bottom fifth of the plot. |
showBrush | boolean | false | Draw a range selector under the chart. |
defaultBrushRange | [number, number] | every row | The selector's first range, as row indices. |
onBrushChange | (range: [number, number]) => void | — | Called with the new range, as row indices. |
onActiveIndexChange | (index: number | null) => void | — | Called with the row under the pointer or the keyboard, and null when none is. |
valueFormatter | (value: number) => string | 2–4 decimals | Format a price: axis ticks and tooltip. |
volumeFormatter | (value: number) => string | compact (1.2M) | Format a volume in the tooltip. |
dateFormatter | (date, index) => string | ISO date | Format a row's date: the tooltip heading, and the x ticks unless xTickFormatter is set. |
xTickFormatter / yTickFormatter | (value, index) => string | dateFormatter / valueFormatter | Format tick text alone. |
yDomain | [number | "auto", number | "auto"] | fit the visible candles | Pin either end of the price axis. |
yAxisPosition | "left" | "right" | the inline end | Which side the price axis sits on. |
size | "sm" | "md" | "lg" | "md" | Aspect ratio. |
showGrid / showTooltip / showXAxis / showYAxis | boolean | true | Toggle chart chrome. |
animate | boolean | true | Animate the candles when they enter. Reduced motion turns it off regardless. |
strings | Partial<CandlestickChartStrings> | follows dir | Override the tooltip's words, and selection's: selected, cleared, items, period. |
selectionMode | "none" | "single" | "none" | "single" lets a reader choose one period by click, tap, Enter or Space; choosing it again clears it. |
selected | string | null | — | The date (a Date or a timestamp as its ISO instant) of the chosen period, controlled. null chooses nothing. |
defaultSelected | string | null | null | The date (a Date or a timestamp as its ISO instant) of the period chosen at first render, uncontrolled. |
onSelectedChange | (key, { datum, index, source }) => void | — | Called on every choice and clear; source is "pointer" or "keyboard". |
active | string | null | — | The date (a Date or a timestamp as its ISO instant) of the lit period, controlled: opens the tooltip on it. |
onActiveChange | (key: string | null) => void | — | Called when the chart's own pointer or arrow keys move to another period. |
children | ReactNode | — | Extra Recharts children (ReferenceLine, Line, ReferenceArea, …). |
dir | "ltr" | "rtl" | inherited | Reading direction. Unset, read off the chart's position in the page once it mounts. |
label | string | — | Accessible name; renders the chart as a role="figure". |
description | string | — | Visually hidden summary for assistive tech. |
loading | boolean | false | Render the skeleton. |
error | ReactNode | — | Render an error message instead of the chart. |
emptyState | ReactNode | "No data to display" | Shown when no row has prices. |
Up, down and colour
Colour is never the only thing that tells up from down. A candle that closed above its open is hollow; one that closed below is filled; one that closed where it opened is a hairline across its body. The chart reads the same in greyscale, in print and on a plate that cannot carry a hue.
- Up and down take the status inks: the success ink for up, the error ink for down, and the muted ink for a candle that did not move. They are the same inks a profit-and-loss line uses.
- Fill and colour follow the same rule — the close against the open — so they never disagree.
- In the tooltip the change carries its sign, so the direction is in the text too.
- Volume bars take their candle's ink: a light fill under a full-ink top edge, so the bar's height reads at a mark's contrast and the candles stay the figure.
Missing and sparse data
A row with no prices is a period with no trading. It keeps its slot, draws no candle, and the tooltip says so. It is never drawn at zero.
- Rows sit one per slot, so time does not stretch over weekends or holidays. Leave a row out to close a gap, or keep it without prices to show one.
- A row whose high and low are given the wrong way round is drawn around every price it gives.
- When no row has prices, the chart shows
emptyState.
Accessibility
- Give every chart a
label. It names the figure to assistive tech. State the insight, not the anatomy: "Shares fell $3.86 on April 7", not "Candlestick chart of daily prices". - Put the numbers in
description. A static read should carry the message without anyone stepping through the days. - Offer the rows as a table when the exact prices matter (Accessible name and data table).
- Keyboard. Tab moves focus to the chart, which draws the focus ring. The Left and Right arrow keys step through every period from the start of the axis to its end, and each is read through the tooltip: the date, then open, high, low, close, the change and the volume. Escape dismisses the tooltip without leaving the chart. The range selector's handles are sliders: Tab to one, then the arrow keys move it.
- Pointer. Anywhere in a candle's column reads that candle, and the range selector's handles take a 44px target under a finger.
- Motion. Candles grow in once. Under
prefers-reduced-motionthey do not, and the dimming of the other candles is instant. - Screen readers. Not yet verified with VoiceOver, NVDA or JAWS, which is why the component is in beta.
- With
selectionMode="single"the periods become a list to choose from. The chart's one tab stop is a namedlistbox("ACME: Periods"), each period anoptionwitharia-selectedand a name that carries its readings, and the period the arrow keys are on is itsaria-activedescendant. Enter or Space chooses that period, so Enter no longer toggles the tooltip;Escapestill hides it. A polite status region says "Mar 3 selected" or "Selection cleared" once per change. The screen reader pass of this mode is still to be run.
Composition
children are rendered inside the Recharts chart and share its axes, so an
annotation is Recharts' own API instead of a prop:
import { Line, ReferenceLine } from "recharts";
<CandlestickChart data={rows}>
<ReferenceLine y={last} strokeDasharray="4 4" />
<Line dataKey="average" stroke="var(--chart-1)" dot={false} tooltipType="none" />
</CandlestickChart>A reference line left at its default ink is painted as a hairline to match the
grid; a stroke you pass is kept. A label is text, so give it a text ink
(hsl(var(--muted-foreground))).
Boundaries
- No indicators of its own. The chart computes nothing: an average, a band or a signal is your data, drawn as a child.
- No crosshair price tags, drawing tools or indicator panes. Those belong to a trading terminal.
- No colour by previous close. Fill and colour both compare a candle's close with its own open.
- No second price axis. Draw two charts.
Theming
Up and down come from the status tokens, so both themes are handled and a plate
re-lights them. Axis text, grid lines, the range selector and the tooltip use
the page's text, border and surface tokens, and follow a coloured Surface
without a prop.