Price Chart
betaA 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
down
no volume
loading
Loading chart
empty
No trades yet
error
Prices could not load.
Prices could not load.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. 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
| Prop | Type | Default | What it does |
|---|---|---|---|
| data | { time: number | string | Date; close: number; open?, high?, low?, volume?: number | null }[] | - | 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. |
| baseline | number | - | The dashed line the price is measured from. Default: the first close. |
| title | ReactNode | - | A visible title. It names the chart. TypeScript requires title, aria-label, or aria-labelledby. |
| aria-label | string | - | Names the chart when it has no visible title. |
| valueFormat | Intl.NumberFormatOptions | US dollars, with three significant digits when every price is under a cent | Formats prices in the tooltip, and on the axis in short form. The axis adds digits when short labels would repeat. |
| timeFormat | Intl.DateTimeFormatOptions | - | Formats the tooltip's time. Default: the time of day for points within a day, the date for daily points. |
| tickFormat | Intl.DateTimeFormatOptions | - | Formats the time axis. Default: hours for a day, days up to two months, months up to two years, then years. |
| timeZone | string | "UTC" | 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. |
| locale | string | "en-US" | The locale for numbers and dates. |
| volume | boolean | true when any point has volume | Shows volume bars along the bottom. |
| labels | { close?, open?, high?, low?, volume?, baseline?, change?: string } | baseline "Start" | The words for each value. Pass baseline: "Previous close" for a day. |
| size | "sm" | "md" | "lg" | "md" | Height: 5, 7, or 9 times --control-h-md, so density scales it. |
| loading | boolean | false | Shows a soft area shape at the same height. When data arrives, it fades out while the price draws in. |
| error | ReactNode | - | Shows this message in place of the chart. true shows a default message. |
| onRetry | () => void | - | Shows a retry button under the error. |
| emptyText | ReactNode | "No data yet" | Shown when there are no points. |
| className | string | - | Extra classes. Later classes win, so className='h-96' replaces the height. |
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} />