Candlestick Chart

Open, high, low and close for a period, drawn as one mark.

'use client';

import { useState } from 'react';
import { CandlestickChart, type CandlestickChartDatum } from '@/components/panelui/candlestick-chart';
import { Frame } from '@/components/panelui/frame';

const SESSIONS = [
  { day: '1 Sep', open: 182.4, high: 185.1, low: 181.2, close: 184.6 },
  { day: '2 Sep', open: 184.6, high: 186.0, low: 182.9, close: 183.1 },
  { day: '3 Sep', open: 183.1, high: 184.4, low: 180.7, close: 181.5 },
  { day: '4 Sep', open: 181.5, high: 183.9, low: 180.9, close: 183.4 },
  { day: '5 Sep', open: 183.4, high: 187.2, low: 183.0, close: 186.8 },
  { day: '8 Sep', open: 186.8, high: 188.3, low: 185.6, close: 187.9 },
  { day: '9 Sep', open: 187.9, high: 189.0, low: 186.1, close: 186.6 },
  { day: '10 Sep', open: 186.6, high: 187.4, low: 183.8, close: 184.2 },
  { day: '11 Sep', open: 184.2, high: 184.9, low: 181.3, close: 182.0 },
  { day: '12 Sep', open: 182.0, high: 183.2, low: 179.6, close: 180.4 },
  { day: '15 Sep', open: 180.4, high: 182.7, low: 179.9, close: 182.3 },
  { day: '16 Sep', open: 182.3, high: 185.5, low: 182.0, close: 185.0 },
  { day: '17 Sep', open: 185.0, high: 186.4, low: 184.1, close: 185.0 },
  { day: '18 Sep', open: 185.0, high: 188.8, low: 184.7, close: 188.2 },
  { day: '19 Sep', open: 188.2, high: 191.3, low: 187.6, close: 190.7 },
  { day: '22 Sep', open: 190.7, high: 192.1, low: 189.2, close: 189.8 },
  { day: '23 Sep', open: 189.8, high: 190.6, low: 186.9, close: 187.4 },
  { day: '24 Sep', open: 187.4, high: 189.5, low: 186.8, close: 189.1 },
  { day: '25 Sep', open: 189.1, high: 193.0, low: 188.7, close: 192.4 },
  { day: '26 Sep', open: 192.4, high: 194.6, low: 191.5, close: 193.9 },
  { day: '29 Sep', open: 193.9, high: 195.2, low: 191.8, close: 192.3 },
  { day: '30 Sep', open: 192.3, high: 193.1, low: 189.4, close: 190.0 },
  { day: '1 Oct', open: 190.0, high: 191.7, low: 188.2, close: 191.2 },
  { day: '2 Oct', open: 191.2, high: 194.3, low: 190.8, close: 193.8 },
  { day: '3 Oct', open: 193.8, high: 197.0, low: 193.2, close: 196.4 },
  { day: '6 Oct', open: 196.4, high: 198.1, low: 195.0, close: 195.6 },
  { day: '7 Oct', open: 195.6, high: 196.2, low: 192.7, close: 193.3 },
  { day: '8 Oct', open: 193.3, high: 195.8, low: 192.9, close: 195.2 },
  { day: '9 Oct', open: 195.2, high: 198.9, low: 194.8, close: 198.3 },
  { day: '10 Oct', open: 198.3, high: 200.4, low: 197.1, close: 199.6 },
];

const last = SESSIONS[SESSIONS.length - 1];
const price = (value: number) => `$${value.toFixed(2)}`;

export default function CandlestickChartOverview() {
  const [active, setActive] = useState<CandlestickChartDatum | null>(null);

  return (
    <Frame variant="band" className="w-full max-w-lg">
      <Frame.Header>
        <Frame.Title>NWM</Frame.Title>
        <Frame.Action>Daily</Frame.Action>
      </Frame.Header>
      <Frame.Panel className="p-5">
        <CandlestickChart
          data={SESSIONS}
          xDataKey="day"
          aspectRatio={1.5}
          label="NWM daily prices, 1 September to 10 October"
          onActiveIndexChange={(_index, datum) => setActive(datum)}
        >
          <CandlestickChart.Header
            title={active ? String(active.day) : 'Last close'}
            value={price(active ? Number(active.close) : last.close)}
            legend
          />
          <CandlestickChart.Grid />
          <CandlestickChart.Candles />
          <CandlestickChart.YAxis format={(value) => value.toFixed(0)} />
          <CandlestickChart.XAxis />
          <CandlestickChart.Tooltip formatValue={price} />
        </CandlestickChart>
      </Frame.Panel>
    </Frame>
  );
}

Use it to show how a price moved within each period: a trading day, an hour, a month. Each row of data holds an open, a high, a low and a close.

The body spans open to close, and the wick behind it spans low to high. A period that closed at or above its open is drawn in the rising colour, and one that closed below it in the falling colour.

The price axis does not reach zero, so candles stay readable when the prices sit far from it. For a single value per period, use a line chart instead.

Installation

npx shadcn@latest add https://panelui.dev/r/web/candlestick-chart.json

The source is copied into components/panelui/, where you can edit it.

Usage

import { CandlestickChart } from "@/components/panelui/candlestick-chart"
<CandlestickChart data={sessions} xDataKey="day">
  <CandlestickChart.Grid />
  <CandlestickChart.Candles />
  <CandlestickChart.YAxis />
  <CandlestickChart.XAxis />
  <CandlestickChart.Tooltip />
</CandlestickChart>

Composition

<CandlestickChart data={…}>
  <CandlestickChart.Header />
  <CandlestickChart.Grid />
  <CandlestickChart.Candles />
  <CandlestickChart.XAxis />
  <CandlestickChart.YAxis />
  <CandlestickChart.Legend />
  <CandlestickChart.Tooltip />
</CandlestickChart>
  • CandlestickChart.Header — The strip above the plot: title, value and caption, with the two colours or your own control on the trailing side.
  • CandlestickChart.Grid — Lines across the price axis, in the theme's border colour. rows is how many intervals to draw. With a YAxis in the chart, its ticks sets the count, so every label has a line.
  • CandlestickChart.Candles — The marks. Without this part the chart draws no candles. risingColor and fallingColor name theme roles.
  • CandlestickChart.XAxis — Period labels under the candles. It keeps every nth label, as many as the width allows. ticks asks for a count.
  • CandlestickChart.YAxis — Price labels on the grid lines, with room made for them. format writes a price.
  • CandlestickChart.Legend — The two colours and what they mean, over the top corner of the plot. On a chart with a header, use the header's legend prop instead.
  • CandlestickChart.Tooltip — The readout, and what drives it: the four prices and the change from open to close. It covers the plot, so the pointer does not have to land on a candle.

Examples

Naming the four columns

The prices are read from open, high, low and close by default. If your data uses other names, point openDataKey, highDataKey, lowDataKey and closeDataKey at them.

'use client';

import { CandlestickChart } from '@/components/panelui/candlestick-chart';

const BARS = [
  { t: '25 Sep', o: 189.1, h: 193.0, l: 188.7, c: 192.4 },
  { t: '26 Sep', o: 192.4, h: 194.6, l: 191.5, c: 193.9 },
  { t: '29 Sep', o: 193.9, h: 195.2, l: 191.8, c: 192.3 },
  { t: '30 Sep', o: 192.3, h: 193.1, l: 189.4, c: 190.0 },
  { t: '1 Oct', o: 190.0, h: 191.7, l: 188.2, c: 191.2 },
  { t: '2 Oct', o: 191.2, h: 194.3, l: 190.8, c: 193.8 },
  { t: '3 Oct', o: 193.8, h: 197.0, l: 193.2, c: 196.4 },
  { t: '6 Oct', o: 196.4, h: 198.1, l: 195.0, c: 195.6 },
  { t: '7 Oct', o: 195.6, h: 196.2, l: 192.7, c: 193.3 },
  { t: '8 Oct', o: 193.3, h: 195.8, l: 192.9, c: 195.2 },
];

export default function CandlestickChartColumnKeys() {
  return (
    <CandlestickChart
      data={BARS}
      xDataKey="t"
      openDataKey="o"
      highDataKey="h"
      lowDataKey="l"
      closeDataKey="c"
      aspectRatio={1.9}
      className="max-w-lg"
    >
      <CandlestickChart.Grid />
      <CandlestickChart.Candles />
      <CandlestickChart.YAxis />
      <CandlestickChart.XAxis />
      <CandlestickChart.Tooltip />
    </CandlestickChart>
  );
}

Colours of your own

risingColor and fallingColor name roles in the theme. The default success and destructive roles are the green and red most readers expect, so change them when a chart has to match something else.

'use client';

import { CandlestickChart } from '@/components/panelui/candlestick-chart';

const SESSIONS = [
  { day: '25 Sep', open: 189.1, high: 193.0, low: 188.7, close: 192.4 },
  { day: '26 Sep', open: 192.4, high: 194.6, low: 191.5, close: 193.9 },
  { day: '29 Sep', open: 193.9, high: 195.2, low: 191.8, close: 192.3 },
  { day: '30 Sep', open: 192.3, high: 193.1, low: 189.4, close: 190.0 },
  { day: '1 Oct', open: 190.0, high: 191.7, low: 188.2, close: 191.2 },
  { day: '2 Oct', open: 191.2, high: 194.3, low: 190.8, close: 193.8 },
  { day: '3 Oct', open: 193.8, high: 197.0, low: 193.2, close: 196.4 },
  { day: '6 Oct', open: 196.4, high: 198.1, low: 195.0, close: 195.6 },
  { day: '7 Oct', open: 195.6, high: 196.2, low: 192.7, close: 193.3 },
  { day: '8 Oct', open: 193.3, high: 195.8, low: 192.9, close: 195.2 },
];

export default function CandlestickChartColors() {
  return (
    <CandlestickChart data={SESSIONS} xDataKey="day" aspectRatio={1.9} className="max-w-lg">
      <CandlestickChart.Grid />
      <CandlestickChart.Candles risingColor="info" fallingColor="warning" cornerRadius={3} />
      <CandlestickChart.YAxis />
      <CandlestickChart.XAxis />
      <CandlestickChart.Legend risingLabel="Gain" fallingLabel="Loss" />
      <CandlestickChart.Tooltip />
    </CandlestickChart>
  );
}

Sized for a card

Leave out the grid and the price axis for a chart that is read at a glance. A wider candleGap suits a short run of periods. It is a fraction of each slice, so the proportions hold at any width.

'use client';

import { CandlestickChart } from '@/components/panelui/candlestick-chart';

const RECENT = [
  { day: '25 Sep', open: 189.1, high: 193.0, low: 188.7, close: 192.4 },
  { day: '26 Sep', open: 192.4, high: 194.6, low: 191.5, close: 193.9 },
  { day: '29 Sep', open: 193.9, high: 195.2, low: 191.8, close: 192.3 },
  { day: '30 Sep', open: 192.3, high: 193.1, low: 189.4, close: 190.0 },
  { day: '1 Oct', open: 190.0, high: 191.7, low: 188.2, close: 191.2 },
  { day: '2 Oct', open: 191.2, high: 194.3, low: 190.8, close: 193.8 },
  { day: '3 Oct', open: 193.8, high: 197.0, low: 193.2, close: 196.4 },
  { day: '6 Oct', open: 196.4, high: 198.1, low: 195.0, close: 195.6 },
  { day: '7 Oct', open: 195.6, high: 196.2, low: 192.7, close: 193.3 },
  { day: '8 Oct', open: 193.3, high: 195.8, low: 192.9, close: 195.2 },
];

export default function CandlestickChartCard() {
  return (
    <CandlestickChart data={RECENT} xDataKey="day" aspectRatio={1.9} candleGap={0.42} className="max-w-sm">
      <CandlestickChart.Header value="$195.20" caption="+1.9 today" />
      <CandlestickChart.Candles cornerRadius={2} />
      <CandlestickChart.XAxis ticks={4} />
    </CandlestickChart>
  );
}

Loading, then data

With status="loading" the grid stays and no candles are drawn, because a placeholder candle would show prices that do not exist. The candles grow in from the left when it turns ready.

'use client';

import { useState } from 'react';
import { Button } from '@/components/panelui/button';
import { CandlestickChart, type CandlestickChartStatus } from '@/components/panelui/candlestick-chart';

const RECENT = [
  { day: '25 Sep', open: 189.1, high: 193.0, low: 188.7, close: 192.4 },
  { day: '26 Sep', open: 192.4, high: 194.6, low: 191.5, close: 193.9 },
  { day: '29 Sep', open: 193.9, high: 195.2, low: 191.8, close: 192.3 },
  { day: '30 Sep', open: 192.3, high: 193.1, low: 189.4, close: 190.0 },
  { day: '1 Oct', open: 190.0, high: 191.7, low: 188.2, close: 191.2 },
  { day: '2 Oct', open: 191.2, high: 194.3, low: 190.8, close: 193.8 },
  { day: '3 Oct', open: 193.8, high: 197.0, low: 193.2, close: 196.4 },
  { day: '6 Oct', open: 196.4, high: 198.1, low: 195.0, close: 195.6 },
  { day: '7 Oct', open: 195.6, high: 196.2, low: 192.7, close: 193.3 },
  { day: '8 Oct', open: 193.3, high: 195.8, low: 192.9, close: 195.2 },
];

export default function CandlestickChartLoading() {
  const [status, setStatus] = useState<CandlestickChartStatus>('loading');

  return (
    <div className="flex w-full max-w-lg flex-col items-start gap-4">
      <CandlestickChart data={RECENT} status={status} xDataKey="day" aspectRatio={1.9}>
        <CandlestickChart.Grid />
        <CandlestickChart.Candles />
        <CandlestickChart.YAxis />
        <CandlestickChart.XAxis />
      </CandlestickChart>
      <Button variant="outline" onClick={() => setStatus(status === 'loading' ? 'ready' : 'loading')}>
        {status === 'loading' ? 'Finish loading' : 'Load again'}
      </Button>
    </div>
  );
}

Changing data

When data changes, the candles and the axis move to their new values over domainDuration. A ref on the chart has replay(), which grows the candles in again.

'use client';

import { useRef, useState } from 'react';
import { Button } from '@/components/panelui/button';
import { CandlestickChart, type CandlestickChartHandle } from '@/components/panelui/candlestick-chart';

const TICKERS = {
  NWM: [
    { day: '25 Sep', open: 189.1, high: 193.0, low: 188.7, close: 192.4 },
    { day: '26 Sep', open: 192.4, high: 194.6, low: 191.5, close: 193.9 },
    { day: '29 Sep', open: 193.9, high: 195.2, low: 191.8, close: 192.3 },
    { day: '30 Sep', open: 192.3, high: 193.1, low: 189.4, close: 190.0 },
    { day: '1 Oct', open: 190.0, high: 191.7, low: 188.2, close: 191.2 },
    { day: '2 Oct', open: 191.2, high: 194.3, low: 190.8, close: 193.8 },
    { day: '3 Oct', open: 193.8, high: 197.0, low: 193.2, close: 196.4 },
    { day: '6 Oct', open: 196.4, high: 198.1, low: 195.0, close: 195.6 },
    { day: '7 Oct', open: 195.6, high: 196.2, low: 192.7, close: 193.3 },
    { day: '8 Oct', open: 193.3, high: 195.8, low: 192.9, close: 195.2 },
  ],
  ORX: [
    { day: '9 Sep', open: 127.5, high: 128.8, low: 125.5, close: 126.7 },
    { day: '10 Sep', open: 126.7, high: 127.8, low: 124.1, close: 125.2 },
    { day: '11 Sep', open: 125.2, high: 126.2, low: 122.5, close: 123.8 },
    { day: '12 Sep', open: 123.8, high: 125.2, low: 121.5, close: 122.8 },
    { day: '15 Sep', open: 122.8, high: 124.9, low: 121.6, close: 124.0 },
    { day: '16 Sep', open: 124.0, high: 126.6, low: 122.9, close: 125.7 },
    { day: '17 Sep', open: 125.7, high: 127.2, low: 124.2, close: 125.7 },
    { day: '18 Sep', open: 125.7, high: 128.7, low: 124.6, close: 127.7 },
    { day: '19 Sep', open: 127.7, high: 130.2, low: 126.4, close: 129.2 },
    { day: '22 Sep', open: 129.2, high: 130.7, low: 127.4, close: 128.7 },
  ],
};
type Ticker = keyof typeof TICKERS;

export default function CandlestickChartChangingData() {
  const chart = useRef<CandlestickChartHandle>(null);
  const [ticker, setTicker] = useState<Ticker>('NWM');

  return (
    <div className="flex w-full max-w-lg flex-col items-start gap-4">
      <CandlestickChart ref={chart} data={TICKERS[ticker]} xDataKey="day" aspectRatio={1.9}>
        <CandlestickChart.Header title={ticker} />
        <CandlestickChart.Grid />
        <CandlestickChart.Candles />
        <CandlestickChart.YAxis />
        <CandlestickChart.XAxis />
        <CandlestickChart.Tooltip />
      </CandlestickChart>
      <div className="flex gap-2">
        <Button variant="outline" onClick={() => setTicker(ticker === 'NWM' ? 'ORX' : 'NWM')}>
          Show {ticker === 'NWM' ? 'ORX' : 'NWM'}
        </Button>
        <Button variant="ghost" onClick={() => chart.current?.replay()}>
          Replay
        </Button>
      </div>
    </div>
  );
}

API Reference

CandlestickChart

PropTypeDefaultDescription
datareadonly CandlestickChartDatum[]—The rows. Each one is a period along the x-axis.
xDataKeystring'date'Key holding the period label. Used by the axis and the readout.
openDataKeystring'open'Key holding the opening price.
highDataKeystring'high'Key holding the period's high.
lowDataKeystring'low'Key holding the period's low.
closeDataKeystring'close'Key holding the closing price.
statusCandlestickChartStatus'ready'loading leaves the plot empty: the grid and the header stay, and no candles are drawn. Turning ready grows them in from the left. Nothing stands in for the candles, because a placeholder candle would be four made-up prices.
aspectRationumber1.6The plot's width ÷ height. 1.6 suits a chart this dense better than 2.
animationDurationnumber700Milliseconds for the candles to grow in.
domainDurationnumber500Milliseconds for the candles and the axis to settle after the data changes.
yDomain[number, number]—Fix the price axis instead of deriving it from the lows and highs.
candleGapnumber0.3Fraction of each period's slice left empty, 0 to 1.
candleWidthnumber—Fixed body width in pixels, instead of deriving it from candleGap. One pixel always stays between bodies.
fadedOpacitynumber0.3Opacity of the candles that are not being read.
onActiveIndexChange(index: number, datum: CandlestickChartDatum | null) => void—The period being read, and -1/null when there is none. Fires when the index changes, not per frame.
compactbooleanfalseDrop the axis padding, for a dense strip with no axis or readout.
labelstring—What a screen reader calls the chart.
labelForDatum(datum: CandlestickChartDatum, index: number) => string—What a screen reader says for one row. Defaults to its keys and values.

CandlestickChart.Grid

PropTypeDefaultDescription
rowsnumber—How many intervals to divide the price axis into. Yields rows + 1 lines. A YAxis beside it sets the count instead.
dashArraystring—The stroke's dash pattern.
opacitynumber—Opacity of the lines.

CandlestickChart.Candles

PropTypeDefaultDescription
risingColorCandlestickChartColor'success'The theme role of a period that closed at or above its open.
fallingColorCandlestickChartColor'destructive'The theme role of a period that closed below its open.
cornerRadiusnumber1.5Corner radius on a body.

CandlestickChart.XAxis

PropTypeDefaultDescription
ticksnumber—How many labels to show. Derived from the room available when omitted.
format(datum: CandlestickChartDatum, index: number) => string—Format a row's label. Defaults to the value at xDataKey.

CandlestickChart.YAxis

PropTypeDefaultDescription
ticksnumber—How many intervals to divide the price axis into. Yields ticks + 1 labels.
format(value: number) => string—Format a price for its label. Defaults to a compact number.

CandlestickChart.Tooltip

PropTypeDefaultDescription
formatValue(value: number, field: TooltipField) => string—Format one of the four prices. Defaults to a compact number.
formatX(datum: CandlestickChartDatum) => string—Format the readout's heading from the row. Defaults to the value at xDataKey.
showChangeboolean—Show the period's change from open to close under the four prices.
classNamestring—Added to the readout's surface.

CandlestickChart.Legend

PropTypeDefaultDescription
risingLabelstring'Up'What the rising colour is called.
fallingLabelstring'Down'What the falling colour is called.

CandlestickChart.Header

PropTypeDefaultDescription
titleReactNode—Small line above the value: what the chart is of.
valueReactNode—The readout. The largest thing on the card, and the first thing read.
captionReactNode—One muted line under the value: a period, a comparison, a change.
legendbooleanfalseName the two colours along the trailing edge.
risingLabelstring'Up'What the rising colour is called.
fallingLabelstring'Down'What the falling colour is called.

CandlestickChart, CandlestickChart.Header and CandlestickChart.Legend also take the props of the element they render, and a className. CandlestickChart.Tooltip puts its className on the readout. The other parts draw nothing themselves and take only the props listed.

Notes

The price axis

The axis runs from the lowest low to the highest high, with a tenth of the span as margin at each end. It does not include zero, because a share moving between 178 and 183 would otherwise flatten into a band of dashes. yDomain fixes both ends.

Sizing

The chart fills its container's width and takes its height from aspectRatio, which applies to the plot alone, so a header does not shrink it. The plot draws in the browser after it has been measured, and the space is reserved until then, so it is not in server-rendered HTML.

Bundle size

The chart adds about 60 KB of gzipped JavaScript to a page that uses it. A page that does not import it pays nothing, including one that imports other components from the package root.

Missing and flat periods

A row missing any of its four prices draws nothing and keeps its place along the axis. A period that closed where it opened draws a thin flat body, in the rising colour.

Reading a period

With a Tooltip, hovering a period reads it. A click or a tap pins the reading until the next press elsewhere, or Escape. On a touch screen a sideways drag reads the periods it crosses, and a vertical drag scrolls the page.

onActiveIndexChange fires when the period changes, not on every frame, so it can drive a readout outside the chart, as the first example does. Inside the chart, useCandlestickChart gives the same period.

A readout that sets state re-renders the chart, so keep data the same array between renders: a new array redraws the chart and drops the reading. Build it with useMemo, or outside the component.

Keyboard

With a Tooltip, the plot takes focus. The arrow keys move between periods, Home and End go to the first and last, Enter or Space pins the reading, and Escape clears it.

Screen readers

Each row is given as text beside the drawing. label names the chart and labelForDatum writes a row, for values with a unit. Without them a row is read as its keys and values.

The readout's change carries a plus or minus sign, so direction is not told by colour alone.

Reduced motion

With reduced motion set, the candles and the axis appear at their final values.

Server Components

CandlestickChart is a client component. In a file that starts with "use client", write its parts with a dot, as on this page. A Server Component cannot read CandlestickChart.Header off the import, so each part is also exported by name: CandlestickChartHeader is CandlestickChart.Header, and so on for the others.

import { CandlestickChart, CandlestickChartHeader, CandlestickChartGrid, CandlestickChartCandles, CandlestickChartXAxis, CandlestickChartYAxis, CandlestickChartTooltip, CandlestickChartLegend } from "@/components/panelui/candlestick-chart"

On this page