Skip to content
txbrendan/ui

Chart

beta

The base every chart builds on: a frame that holds its height, theme series colors, a themed tooltip and legend, and loading, empty, and error views.

Preview

colors

  • --chart-1
  • --chart-2
  • --chart-3
  • --chart-4
  • --chart-5

loading

Visitors
  • Desktop
  • Mobile

Loading chart

ring

Traffic by source

Loading chart

empty

Visitors
No visitors this week

No visitors this week

error

Visitors

Visitors could not load.

Visitors could not load.

size

Small

Loading the small chart

Medium

Loading the medium chart

Large

Loading the large chart

tooltip

Monday

  • Desktop$1,250
  • Mobile$830

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/chart

Use it for

  • Building a chart we do not ship, like a scatter or radar chart, so it looks and behaves like the others.
  • Put the Recharts chart inside Chart's render function. Spread `name` on the chart, `animation` on each series, and `tooltipAnimation` on the Tooltip.
  • Wrap the Recharts chart in Recharts' AnimationControllerProvider with `animationController`, so it draws in once it is on screen and ends drawn when the page prints.
  • Pass <ChartTooltipContent /> as a Recharts Tooltip's content for a tooltip in theme colors.
  • Use chartSeries(config) for each series' label and color, and chartColor(i) for a single color.
  • Spread chartAxisProps on each axis and chartGridProps on the grid, so they match the charts we ship.
  • Pass a skeleton that looks like your chart, so it loads in its own shape. If you build your own legend, call useChartLegendGlide(ref, waiting) so its items glide when values land.

Avoid it for

  • A bar, line, area, or donut chart. Use BarChart, LineChart, AreaChart, or DonutChart. They already use this frame.
  • A small trend inside a table cell or next to a number. Use Sparkline.
  • A single number. Show the number in text.
  • More than five series. The theme has five chart colors, and a sixth series repeats the first color.

Props

  • title

    A visible title, shown as the figure's caption. It names the chart.

    ReactNode

  • aria-label

    Names the chart when it has no visible title. TypeScript requires title, aria-label, or aria-labelledby.

    string

  • size

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

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

  • loading

    Shows a loading shape at the same height and sets aria-busy on the plot area. When data arrives, the shape fades out while the chart draws under it.

    boolean · default false

  • loadingText

    Read out to screen readers while loading.

    string · default "Loading chart"

  • error

    Shows this message in place of the chart. true shows "The chart could not load."

    ReactNode

  • onRetry

    Shows a retry button under the error.

    () => void

  • retryText

    The retry button's text.

    string · default "Try again"

  • empty

    True when there is nothing to draw. Shows emptyText.

    boolean · default false

  • emptyText

    Shown when the chart is empty.

    ReactNode · default "No data yet"

  • legend

    Legend entries, shown under the chart while it loads and once it draws.

    { key: string; label: ReactNode; color: string; value?: ReactNode }[]

  • skeleton

    The loading shape. Pick the one that looks like the chart: area, line, dots for a swarm, points for a scatter, ring for a pie or donut.

    "bars" | "area" | "line" | "dots" | "points" | "ring" · default "bars"

  • children

    Draws the chart once it is ready. Leave it out to show only the frame and its states.

    ({ animation, tooltipAnimation, animationController, name }) => ReactNode

  • className

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

    string

Accessibility

  • Renders a <figure>. A title becomes its <figcaption>, which names the figure and the chart's SVG. TypeScript requires title, aria-label, or aria-labelledby.
  • Keeps one height in every state, set in CSS, so the page does not move when data arrives. The height is in the server HTML. When the page prints, it asks the browser to keep a chart that fits on one page whole. In the light theme, it asks the browser to print legend dots and loading shapes even with backgrounds off.
  • While loading, the plot area has aria-busy and the loading shapes are hidden from screen readers. With motion off, the shape goes at once. Reduced motion keeps the fade.
  • A role="status" line stays on the page outside the plot area. It reads out loading, the error, and the empty text when they are plain text.
  • The error view shows a retry Button when you pass onRetry, so keyboard users can try again.
  • Recharts' keyboard layer stays on: the chart's SVG is one tab stop, the arrow keys move between points, and the tooltip reads each point out.
  • The SVG takes the focus-ring style, so keyboard focus shows on the chart.
  • Series colors default to --chart-1 through --chart-5. The theme tests hold each to 3:1 against the page background, in light and dark. The legend and tooltip also name each series in text.
  • Series draw in over three times --motion-duration-base, and only once the chart is on screen. A chart off screen waits with no timer running. When the page prints, every series jumps to its last frame, even in a chart never seen. The tooltip moves over --motion-duration-base. At 0ms, motion off, charts draw with no animation. Recharts also skips animation when the system asks for reduced motion.

Examples

A chart we do not ship, on the frame

"use client";

import { AnimationControllerProvider, CartesianGrid, Scatter, ScatterChart, Tooltip, XAxis, YAxis } from "recharts";

import { Chart, ChartTooltipContent, chartAxisProps, chartColor, chartGridProps } from "@/components/ui/chart";

const homes = [
  { size: 80, price: 210 },
  { size: 120, price: 290 },
  { size: 160, price: 410 },
  { size: 210, price: 520 },
];

export function PriceBySize() {
  return (
    <Chart title="Price by size" empty={homes.length === 0}>
      {({ animation, tooltipAnimation, animationController, name }) => (
        <AnimationControllerProvider value={animationController}>
          <ScatterChart responsive className="size-full" data={homes} {...name}>
            <CartesianGrid {...chartGridProps} />
            <XAxis dataKey="size" name="Size" unit=" m²" type="number" {...chartAxisProps} />
            <YAxis dataKey="price" name="Price" unit="K" type="number" {...chartAxisProps} />
            <Tooltip content={<ChartTooltipContent hideLabel />} cursor={{ stroke: "var(--input)" }} {...tooltipAnimation} />
            <Scatter name="Homes" fill={chartColor(0)} {...animation} />
          </ScatterChart>
        </AnimationControllerProvider>
      )}
    </Chart>
  );
}

Loading, empty, and error

<Chart title="Sales" loading />

<Chart title="Sales" empty emptyText="No sales this week" />

<Chart title="Sales" error="Sales could not load." onRetry={() => refetch()} />

A legend and a tooltip in theme colors

import { ChartLegend, ChartTooltipContent, chartSeries } from "@/components/ui/chart";

const series = chartSeries({ desktop: { label: "Desktop" }, mobile: { label: "Mobile" } });

<ChartLegend items={series} />

<Tooltip content={<ChartTooltipContent valueFormat={{ style: "currency", currency: "USD" }} />} />