Skip to content
txbrendan/ui

Price Chart

beta

A price over time against a baseline like the previous close: green above it, red below, with volume and each point's open, high, low, and close.

Preview

range

Price over the range

down

A day that closed below the previous close

no volume

Price alone

loading

Price over the range

Loading chart

empty

Price over the range
No trades yet

No trades yet

error

Price over the range

Prices could not load.

Prices could not load.

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/price-chart

Use it for

  • Showing how a price moved over a day, a week, or a year, like a stock, a coin, or a fund.
  • Pass data with a time and a close for each point, oldest first. Add open, high, low, and volume to show them in the tooltip.
  • Pass baseline, like the previous close, for the dashed line. Above it the line is green, below it red. Left out, the first close is the baseline.
  • Switch ranges, as from tabs, and the new range draws in from the left as the old one fades. A new first point counts as a new range.
  • Pass timeZone, like "America/New_York", to show times where the market is.

Avoid it for

  • Several series at once. Use LineChart.
  • Values that are not prices or have no baseline worth comparing to. Use AreaChart.
  • A tiny trend in a table cell. Use Sparkline.

Props

  • data

    One point per time, oldest first. Points are spaced evenly, so nights and weekends leave no gap. A date and time with no zone, like "2026-10-29T14:30", is read as UTC. A point with no time is left out.

    { time: number | string | Date; close: number; open?, high?, low?, volume?: number | null }[]

  • baseline

    The dashed line the price is measured from. Default: the first close.

    number

  • title

    A visible title. It names the chart. TypeScript requires title, aria-label, or aria-labelledby.

    ReactNode

  • aria-label

    Names the chart when it has no visible title.

    string

  • valueFormat

    Formats prices in the tooltip, and on the axis in short form. The axis adds digits when short labels would repeat.

    Intl.NumberFormatOptions · default US dollars, with three significant digits when every price is under a cent

  • timeFormat

    Formats the tooltip's time. Default: the time of day for points within a day, the date for daily points.

    Intl.DateTimeFormatOptions

  • tickFormat

    Formats the time axis. Default: hours for a day, days up to two months, months up to two years, then years.

    Intl.DateTimeFormatOptions

  • timeZone

    The time zone for every date. Fixed, so server and browser print the same text. One the browser does not know falls back to UTC.

    string · default "UTC"

  • locale

    The locale for numbers and dates.

    string · default "en-US"

  • volume

    Shows volume bars along the bottom.

    boolean · default true when any point has volume

  • labels

    The words for each value. Pass baseline: "Previous close" for a day.

    { close?, open?, high?, low?, volume?, baseline?, change?: string } · default baseline "Start"

  • size

    Height: 5, 7, or 9 times --control-h-md, so density scales it.

    "sm" | "md" | "lg" · default "md"

  • loading

    Shows a soft area shape at the same height. When data arrives, it fades out while the price draws in.

    boolean · default false

  • error

    Shows this message in place of the chart. true shows a default message.

    ReactNode

  • onRetry

    Shows a retry button under the error.

    () => void

  • emptyText

    Shown when there are no points.

    ReactNode · default "No data yet"

  • className

    Extra classes. Later classes win, so className='h-96' replaces the height.

    string

Accessibility

  • Renders a <figure> named by its title, or by aria-label. TypeScript requires one.
  • A hidden line reads the range at once: the last close, the change from the baseline, and the high and low.
  • Tab focuses the chart. The arrow keys step through the points, and the tooltip reads out each one: its time, close, open, high, low, volume, and the change from the baseline.
  • Up and down are said as a signed percent in the tooltip, not by color alone. The baseline is named in its pill.
  • A line glides to the point under the pointer, and its dot grows in. The price labels fade back and the pill steps aside while it is read, since the tooltip carries the numbers. Escape hides the tooltip, the line, and the dot, and brings the labels back.
  • A range on its way out takes no focus and no pointer, so Tab always lands on the chart being shown. Right to left, time still runs left to right. A long baseline name is cut short.
  • The line draws in from the left once the chart is on screen. A new range draws in the same way as the old one fades. At 0ms everything draws at once, and Recharts skips the motion for reduced motion.
  • Keeps one height while it loads, draws, fails, or has no data, so the page does not move.

Examples

A day against the previous close

<PriceChart
  title="Price today"
  data={points}
  baseline={279.85}
  timeZone="America/New_York"
  labels={{ baseline: "Previous close" }}
/>

Ranges that follow tabs

const [range, setRange] = useState("1D");

<Tabs value={range} onValueChange={(next) => setRange(String(next))}>
  <TabsList aria-label="Range">
    <TabsTab value="1D">1D</TabsTab>
    <TabsTab value="1M">1M</TabsTab>
    <TabsTab value="1Y">1Y</TabsTab>
  </TabsList>
  <TabsPanel value={range}>
    <PriceChart title="Price over the range" data={byRange[range]} baseline={range === "1D" ? previousClose : undefined} />
  </TabsPanel>
</Tabs>

While it loads

<PriceChart title="Price today" data={data ?? []} loading={isLoading} />