---
title: Chart
description: "Charts built on Recharts: a config-driven container plus tooltip and legend bodies, styled with the design tokens."
sidebar:
  badge: New
---

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts';

import { container, fontSize } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

const revenue = [
  { month: 'Jan', revenue: 18600 },
  { month: 'Feb', revenue: 30500 },
  { month: 'Mar', revenue: 23700 },
  { month: 'Apr', revenue: 7300 },
  { month: 'May', revenue: 20900 },
  { month: 'Jun', revenue: 21400 },
];

// The config is the single source for the series label and its color: the
// tooltip reads the label from here, and the mark below is filled from the
// same entry.
const chartConfig = {
  revenue: { label: 'Revenue', color: colors.chart1 },
} satisfies ChartConfig;

export default function ChartDemo() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      <BarChart accessibilityLayer data={revenue} margin={{ left: 12, right: 12 }}>
        <CartesianGrid vertical={false} stroke={colors.border} strokeDasharray="4 4" />
        <XAxis
          dataKey="month"
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          tick={{ fill: colors.mutedForeground, fontSize: fontSize.xs }}
        />
        <ChartTooltip
          cursor={{ fill: colors.accent }}
          content={<ChartTooltipContent />}
        />
        <ChartLegend itemSorter={null} content={<ChartLegendContent />} />
        {/* Rounded only at the data end, so the bar stays anchored to the baseline. */}
        <Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[4, 4, 0, 0]} />
      </BarChart>
    </ChartContainer>
  );
}

const styles = stylex.create({
  chart: {
    maxWidth: container.xxl,
  },
});
```

## Install

```bash
npx @madeui/cli add chart
```

Installs `recharts` alongside the component.

> **Charts render in the browser**
>
> Recharts measures its container before it can lay a plot out, so a chart is empty until it mounts on the client. `ChartContainer` reserves the box with `aspect-ratio`, so the space is held from the first paint and nothing on the page moves when the plot arrives. In the Next.js App Router, any file that renders a chart needs `'use client'` at the top.

## Usage

```tsx
import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  useChart,
  type ChartConfig,
} from '@/components/ui/chart';
```

```tsx
'use client';

import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts';

import { colors } from '@/lib/tokens.stylex';

const chartConfig = {
  revenue: { label: 'Revenue', color: colors.chart1 },
} satisfies ChartConfig;

<ChartContainer config={chartConfig}>
  <BarChart accessibilityLayer data={rows}>
    <CartesianGrid vertical={false} stroke={colors.border} strokeDasharray="4 4" />
    <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} />
    <ChartTooltip content={<ChartTooltipContent />} />
    <ChartLegend content={<ChartLegendContent />} />
    <Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[4, 4, 0, 0]} />
  </BarChart>
</ChartContainer>;
```

The config is the one place a series is named and colored. `ChartTooltipContent` and `ChartLegendContent` read the label and the swatch color out of it, and the mark is filled from the same entry — so a series never drifts between the plot and the chrome. Keys are the series' `dataKey`; on a pie or a radial bar, where every slice comes from one series, they are the values of its `nameKey` instead.

Marks, axes, grids, and the chart element itself are Recharts' own components, composed directly. Nothing here wraps them.

## Composition

```tsx
<ChartContainer config={chartConfig}>
  <BarChart>
    <CartesianGrid />
    <XAxis />
    <ChartTooltip content={<ChartTooltipContent />} />
    <ChartLegend content={<ChartLegendContent />} />
    <Bar />
  </BarChart>
</ChartContainer>
```

## Sizing

`ChartContainer` is a full-width box with an `aspect-ratio` of 16 / 9 and no height of its own. Change the ratio through `style`; a square suits a donut or a radar, and a wide, flat one suits a sparkline.

```tsx
const styles = stylex.create({
  chart: { aspectRatio: '1', maxWidth: container.md },
});

<ChartContainer config={chartConfig} style={styles.chart}>
```

Reach for a fixed `height` only when the surrounding layout already fixes one. A ratio keeps the plot proportionate at every width, and it is what reserves the box before the chart mounts.

## Line

Two series, so the config carries two entries and a legend names both. `type="monotone"` smooths the path without letting it overshoot a data point, and the points stay undotted: the tooltip already marks the hovered month on every line, which is the only place a dot carries information.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts';

import { container, fontSize } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

const visitors = [
  { month: 'Jan', desktop: 186, mobile: 80 },
  { month: 'Feb', desktop: 305, mobile: 200 },
  { month: 'Mar', desktop: 237, mobile: 120 },
  { month: 'Apr', desktop: 173, mobile: 190 },
  { month: 'May', desktop: 209, mobile: 130 },
  { month: 'Jun', desktop: 214, mobile: 140 },
];

// Two series, so the config carries two entries and the legend names both.
const chartConfig = {
  desktop: { label: 'Desktop', color: colors.chart1 },
  mobile: { label: 'Mobile', color: colors.chart2 },
} satisfies ChartConfig;

export default function ChartLine() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      <LineChart accessibilityLayer data={visitors} margin={{ left: 12, right: 12 }}>
        <CartesianGrid vertical={false} stroke={colors.border} strokeDasharray="4 4" />
        <XAxis
          dataKey="month"
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          tick={{ fill: colors.mutedForeground, fontSize: fontSize.xs }}
        />
        <ChartTooltip
          cursor={{ stroke: colors.border }}
          content={<ChartTooltipContent />}
        />
        <ChartLegend itemSorter={null} content={<ChartLegendContent />} />
        {/* No per-point dots: the tooltip already marks the hovered month on
            every line, which is the only place a dot carries information. */}
        <Line
          dataKey="desktop"
          type="monotone"
          stroke={chartConfig.desktop.color}
          strokeWidth={2}
          dot={false}
        />
        <Line
          dataKey="mobile"
          type="monotone"
          stroke={chartConfig.mobile.color}
          strokeWidth={2}
          dot={false}
        />
      </LineChart>
    </ChartContainer>
  );
}

const styles = stylex.create({
  chart: {
    maxWidth: container.xxl,
  },
});
```

## Stacked area

A shared `stackId` stacks the layers and pins their order, so they never swap between updates. The fills are translucent; the stroke keeps each band's top edge legible where two of them meet.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';

import { container, fontSize } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

const signups = [
  { month: 'Jan', free: 420, pro: 120 },
  { month: 'Feb', free: 460, pro: 150 },
  { month: 'Mar', free: 510, pro: 190 },
  { month: 'Apr', free: 480, pro: 230 },
  { month: 'May', free: 560, pro: 260 },
  { month: 'Jun', free: 610, pro: 310 },
];

const chartConfig = {
  free: { label: 'Free', color: colors.chart1 },
  pro: { label: 'Pro', color: colors.chart2 },
} satisfies ChartConfig;

export default function ChartArea() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      <AreaChart accessibilityLayer data={signups} margin={{ left: 12, right: 12 }}>
        <CartesianGrid vertical={false} stroke={colors.border} strokeDasharray="4 4" />
        <XAxis
          dataKey="month"
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          tick={{ fill: colors.mutedForeground, fontSize: fontSize.xs }}
        />
        <ChartTooltip
          cursor={{ stroke: colors.border }}
          content={<ChartTooltipContent indicator="line" />}
        />
        <ChartLegend itemSorter={null} content={<ChartLegendContent />} />
        {/* A shared `stackId` stacks the layers and pins their order, so they
            never swap between updates. The stroke keeps each band's top edge
            legible where two translucent fills meet. */}
        <Area
          dataKey="free"
          type="monotone"
          stackId="signups"
          stroke={chartConfig.free.color}
          strokeWidth={2}
          fill={chartConfig.free.color}
          fillOpacity={0.3}
        />
        <Area
          dataKey="pro"
          type="monotone"
          stackId="signups"
          stroke={chartConfig.pro.color}
          strokeWidth={2}
          fill={chartConfig.pro.color}
          fillOpacity={0.3}
        />
      </AreaChart>
    </ChartContainer>
  );
}

const styles = stylex.create({
  chart: {
    maxWidth: container.xxl,
  },
});
```

## Donut

`innerRadius` opens the hole, and a `<Cell>` per row takes its fill from the config. A stroke in the surface color separates the slices, so the ring reads as one shape cut into parts. The centre total is a `<Label>`, which scales with the plot and never covers a slice.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { Cell, Label, Pie, PieChart } from 'recharts';

import { container, fontSize, fontWeight } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

const sessions = [
  { browser: 'chrome', sessions: 6240 },
  { browser: 'safari', sessions: 1910 },
  { browser: 'firefox', sessions: 820 },
  { browser: 'edge', sessions: 640 },
  { browser: 'other', sessions: 510 },
];

// Every slice comes from one series, so the config is keyed by the values of
// the `nameKey` field rather than by a `dataKey`.
const chartConfig = {
  chrome: { label: 'Chrome', color: colors.chart1 },
  safari: { label: 'Safari', color: colors.chart2 },
  firefox: { label: 'Firefox', color: colors.chart3 },
  edge: { label: 'Edge', color: colors.chart4 },
  // Slot 6 rather than 5: the two smallest slices sit next to each other in
  // the ring, and slots 4 and 5 are close enough in hue to blur there.
  other: { label: 'Other', color: colors.chart6 },
} satisfies ChartConfig;

const count = new Intl.NumberFormat('en-US');
const total = sessions.reduce((sum, row) => sum + row.sessions, 0);

export default function ChartPie() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      <PieChart>
        {/* The slices carry their own labels, so the tooltip needs no title. */}
        <ChartTooltip content={<ChartTooltipContent nameKey="browser" hideLabel />} />
        <Pie
          data={sessions}
          dataKey="sessions"
          nameKey="browser"
          innerRadius="58%"
          // A stroke in the surface color separates the slices, so the ring
          // reads as one shape cut into parts rather than five loose arcs.
          stroke={colors.background}
          strokeWidth={2}
        >
          {sessions.map((row) => (
            <Cell key={row.browser} fill={chartConfig[row.browser as Browser].color} />
          ))}
          <Label content={<CentreTotal />} />
        </Pie>
        <ChartLegend
          itemSorter={null}
          content={<ChartLegendContent nameKey="browser" />}
        />
      </PieChart>
    </ChartContainer>
  );
}

type Browser = keyof typeof chartConfig;

// The centre of a donut is empty, so the total goes there as chart text: it
// scales with the plot and never covers a slice.
function CentreTotal({ viewBox }: { viewBox?: unknown }) {
  const box = viewBox as { cx?: number; cy?: number } | undefined;
  if (box?.cx == null || box.cy == null) return null;
  return (
    <text x={box.cx} y={box.cy} textAnchor="middle" dominantBaseline="middle">
      <tspan
        x={box.cx}
        y={box.cy}
        fill={colors.foreground}
        fontSize={fontSize.xl}
        fontWeight={fontWeight.semibold}
      >
        {count.format(total)}
      </tspan>
      {/* One 24px step below the number, in SVG user units. */}
      <tspan
        x={box.cx}
        y={box.cy + 24}
        fill={colors.mutedForeground}
        fontSize={fontSize.xs}
      >
        Sessions
      </tspan>
    </text>
  );
}

const styles = stylex.create({
  chart: {
    aspectRatio: '1',
    maxWidth: container.lg,
  },
});
```

## Radar

`PolarGrid` and `PolarAngleAxis` draw the web and its labels. Two overlapping shapes need light fills and a visible outline — the outline is what carries the comparison.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from 'recharts';

import { container, fontSize } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

// Short axis labels: they sit outside the web, and a long one is the first
// thing to be clipped when the box narrows.
const scores = [
  { area: 'Speed', current: 92, previous: 78 },
  { area: 'Design', current: 88, previous: 81 },
  { area: 'Support', current: 74, previous: 70 },
  { area: 'Docs', current: 96, previous: 90 },
  { area: 'Pricing', current: 61, previous: 55 },
];

const chartConfig = {
  current: { label: 'This release', color: colors.chart1 },
  previous: { label: 'Previous', color: colors.chart2 },
} satisfies ChartConfig;

export default function ChartRadar() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      {/* A percentage outer radius leaves room for the labels at every width;
          a fixed margin only works at the width it was chosen for. */}
      <RadarChart accessibilityLayer data={scores} outerRadius="72%">
        <PolarGrid stroke={colors.border} />
        <PolarAngleAxis
          dataKey="area"
          tick={{ fill: colors.mutedForeground, fontSize: fontSize.xs }}
        />
        <ChartTooltip cursor={false} content={<ChartTooltipContent />} />
        <ChartLegend itemSorter={null} content={<ChartLegendContent />} />
        {/* Two filled shapes on top of each other muddy both. The reference
            shape is an outline — dashed, so the comparison survives without
            color — and only the current one is filled. */}
        <Radar
          dataKey="previous"
          stroke={chartConfig.previous.color}
          strokeWidth={2}
          strokeDasharray="4 4"
          fill="none"
        />
        <Radar
          dataKey="current"
          stroke={chartConfig.current.color}
          strokeWidth={2}
          fill={chartConfig.current.color}
          fillOpacity={0.2}
        />
      </RadarChart>
    </ChartContainer>
  );
}

const styles = stylex.create({
  chart: {
    aspectRatio: '1',
    maxWidth: container.lg,
  },
});
```

## Radial

One ring per category, longest first. A radial bar's length is harder to compare than a straight one, so the order does the work a shared baseline does on a bar chart, and `background` traces the full ring so each bar reads as a share of it.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { Cell, RadialBar, RadialBarChart } from 'recharts';

import { container } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

const adoption = [
  { plan: 'starter', accounts: 1840 },
  { plan: 'team', accounts: 1290 },
  { plan: 'business', accounts: 760 },
  { plan: 'enterprise', accounts: 310 },
];

const chartConfig = {
  starter: { label: 'Starter', color: colors.chart1 },
  team: { label: 'Team', color: colors.chart2 },
  business: { label: 'Business', color: colors.chart3 },
  enterprise: { label: 'Enterprise', color: colors.chart4 },
} satisfies ChartConfig;

export default function ChartRadial() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      <RadialBarChart
        data={adoption}
        innerRadius="28%"
        outerRadius="96%"
        startAngle={90}
        endAngle={-270}
      >
        <ChartTooltip content={<ChartTooltipContent nameKey="plan" hideLabel />} />
        {/* One ring per category, longest first: a radial bar's length is
            harder to compare than a straight one, so the order does the work
            the shared baseline does on a bar chart. The background traces each
            full ring, so a bar reads as a share of it and needs no gridlines. */}
        <RadialBar dataKey="accounts" background={{ fill: colors.muted }} cornerRadius={4}>
          {adoption.map((row) => (
            <Cell key={row.plan} fill={chartConfig[row.plan as Plan].color} />
          ))}
        </RadialBar>
        <ChartLegend
          itemSorter={null}
          content={<ChartLegendContent nameKey="plan" />}
        />
      </RadialBarChart>
    </ChartContainer>
  );
}

type Plan = keyof typeof chartConfig;

const styles = stylex.create({
  chart: {
    aspectRatio: '1',
    maxWidth: container.md,
  },
});
```

## Legend

`ChartLegend` is the chart's legend; `ChartLegendContent` is its body, and it takes each entry's label and swatch color from the config. `verticalAlign` reaches the body, which pads on the side facing the plot.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts';

import { container, fontSize } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

const sales = [
  { quarter: 'Q1', europe: 120, americas: 98, asia: 64 },
  { quarter: 'Q2', europe: 135, americas: 110, asia: 82 },
  { quarter: 'Q3', europe: 128, americas: 125, asia: 97 },
  { quarter: 'Q4', europe: 150, americas: 140, asia: 115 },
];

const chartConfig = {
  europe: { label: 'Europe', color: colors.chart1 },
  americas: { label: 'Americas', color: colors.chart2 },
  asia: { label: 'Asia', color: colors.chart3 },
} satisfies ChartConfig;

export default function ChartLegendExample() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      <BarChart accessibilityLayer data={sales} margin={{ left: 12, right: 12 }}>
        <CartesianGrid vertical={false} stroke={colors.border} strokeDasharray="4 4" />
        <XAxis
          dataKey="quarter"
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          tick={{ fill: colors.mutedForeground, fontSize: fontSize.xs }}
        />
        <ChartTooltip
          cursor={{ fill: colors.accent }}
          content={<ChartTooltipContent />}
        />
        {/* `verticalAlign` reaches the body, which pads on the side facing the
            plot; the entries take their label and color from the config. */}
        <ChartLegend
          itemSorter={null}
          verticalAlign="top"
          content={<ChartLegendContent />}
        />
        <Bar dataKey="europe" fill={chartConfig.europe.color} radius={[4, 4, 0, 0]} />
        <Bar dataKey="americas" fill={chartConfig.americas.color} radius={[4, 4, 0, 0]} />
        <Bar dataKey="asia" fill={chartConfig.asia.color} radius={[4, 4, 0, 0]} />
      </BarChart>
    </ChartContainer>
  );
}

const styles = stylex.create({
  chart: {
    maxWidth: container.xxl,
  },
});
```

## Tooltip

`labelFormatter` rewrites the title, `formatter` rewrites a row's value, and `indicator="line"` swaps the dot for a bar as tall as its row — the shape to reach for when the rows carry a series stroke.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts';

import { container, fontSize } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from '@/components/ui/chart';

const revenue = [
  { month: 'Jan', subscriptions: 42100, services: 18300 },
  { month: 'Feb', subscriptions: 44800, services: 21900 },
  { month: 'Mar', subscriptions: 47600, services: 19700 },
  { month: 'Apr', subscriptions: 51200, services: 24400 },
  { month: 'May', subscriptions: 53900, services: 26100 },
  { month: 'Jun', subscriptions: 58300, services: 27800 },
];

const chartConfig = {
  subscriptions: { label: 'Subscriptions', color: colors.chart1 },
  services: { label: 'Services', color: colors.chart2 },
} satisfies ChartConfig;

const currency = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  maximumFractionDigits: 0,
});

export default function ChartTooltipExample() {
  return (
    <ChartContainer config={chartConfig} style={styles.chart}>
      <LineChart accessibilityLayer data={revenue} margin={{ left: 12, right: 12 }}>
        <CartesianGrid vertical={false} stroke={colors.border} strokeDasharray="4 4" />
        <XAxis
          dataKey="month"
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          tick={{ fill: colors.mutedForeground, fontSize: fontSize.xs }}
        />
        <ChartTooltip
          cursor={{ stroke: colors.border }}
          content={
            <ChartTooltipContent
              indicator="line"
              labelFormatter={(month) => `${month} 2026`}
              formatter={(value) => currency.format(Number(value))}
            />
          }
        />
        <Line
          dataKey="subscriptions"
          type="monotone"
          stroke={chartConfig.subscriptions.color}
          strokeWidth={2}
          dot={false}
        />
        <Line
          dataKey="services"
          type="monotone"
          stroke={chartConfig.services.color}
          strokeWidth={2}
          dot={false}
        />
      </LineChart>
    </ChartContainer>
  );
}

const styles = stylex.create({
  chart: {
    maxWidth: container.xxl,
  },
});
```

## Sparkline

No axes, no grid, no tooltip: the plot is decoration next to the number that carries the value, and a zero margin lets it fill the box edge to edge. The scale spans the data rather than starting at zero — a sparkline reads as shape, and a zero baseline flattens it. A hidden `<YAxis>` is what carries that domain when no axis is drawn.

```tsx
'use client';

import * as stylex from '@stylexjs/stylex';
import { Area, AreaChart, YAxis } from 'recharts';

import { space, fontSize, fontWeight, lineHeight, container } from '@/lib/constants.stylex';
import { colors } from '@/lib/tokens.stylex';

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { ChartContainer, type ChartConfig } from '@/components/ui/chart';

const activeUsers = [
  { day: 1, users: 1180 },
  { day: 2, users: 1240 },
  { day: 3, users: 1195 },
  { day: 4, users: 1310 },
  { day: 5, users: 1290 },
  { day: 6, users: 1385 },
  { day: 7, users: 1360 },
  { day: 8, users: 1440 },
  { day: 9, users: 1420 },
  { day: 10, users: 1510 },
  { day: 11, users: 1475 },
  { day: 12, users: 1580 },
  { day: 13, users: 1620 },
  { day: 14, users: 1690 },
];

const chartConfig = {
  users: { label: 'Active users', color: colors.chart1 },
} satisfies ChartConfig;

// A sparkline reads as shape, not magnitude, so the scale spans the data
// rather than starting at zero — a zero baseline flattens a 43% rise into a
// straight line. The tenth-of-a-range padding keeps the stroke off the edges.
const users = activeUsers.map((point) => point.users);
const padding = (Math.max(...users) - Math.min(...users)) / 10;
const domain: [number, number] = [
  Math.min(...users) - padding,
  Math.max(...users) + padding,
];

export default function ChartSparkline() {
  return (
    <Card style={styles.card}>
      <CardHeader>
        <CardDescription>Active users</CardDescription>
        <CardTitle style={styles.value}>1,690</CardTitle>
      </CardHeader>
      <CardContent>
        {/* No axes, no grid, no tooltip: the plot is decoration next to the
            number that carries the value. The margin is only what keeps the
            stroke off the edges it would otherwise be clipped by. */}
        <ChartContainer config={chartConfig} style={styles.chart}>
          <AreaChart
            data={activeUsers}
            margin={{ top: 2, right: 2, bottom: 0, left: 2 }}
          >
            <defs>
              <linearGradient id="sparkline-users" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stopColor={chartConfig.users.color} stopOpacity={0.8} />
                <stop offset="100%" stopColor={chartConfig.users.color} stopOpacity={0.05} />
              </linearGradient>
            </defs>
            {/* A hidden axis is what carries the domain when none is drawn. */}
            <YAxis hide domain={domain} />
            <Area
              dataKey="users"
              type="monotone"
              baseValue={domain[0]}
              stroke={chartConfig.users.color}
              strokeWidth={2}
              fill="url(#sparkline-users)"
              isAnimationActive={false}
            />
          </AreaChart>
        </ChartContainer>
        <p {...stylex.props(styles.caption)}>Up 43% over 14 days</p>
      </CardContent>
    </Card>
  );
}

const styles = stylex.create({
  card: {
    width: container.md,
  },
  chart: {
    aspectRatio: '4',
  },
  value: {
    fontSize: fontSize.xl,
    fontVariantNumeric: 'tabular-nums',
    fontWeight: fontWeight.semibold,
    lineHeight: lineHeight.tight,
  },
  caption: {
    color: colors.mutedForeground,
    fontSize: fontSize.xs,
    lineHeight: lineHeight.snug,
    margin: 0,
    marginBlockStart: space.s2,
  },
});
```

## API reference

Built on [Recharts](https://recharts.org). The chart element, marks, axes, grids, and cursors are Recharts' own components, composed directly and documented in the [Recharts API reference](https://recharts.org/en-US/api); this component adds the config, the container, and the tooltip and legend bodies.

No part takes `variant` or `size`: a chart has no meaningful preset axis, and sizing belongs to the container's aspect ratio.

Pass `accessibilityLayer` on the chart element. It makes the plot focusable and steps through the data with the arrow keys, moving the tooltip with the keyboard focus, and it announces each point to a screen reader.

The focus ring on a chart is the browser's own, not the ring the rest of the library draws: the focusable element is the `<svg>` Recharts renders, which no part here owns, and StyleX styles only elements it puts a class on. It is a visible keyboard focus indicator, so it is left as it is rather than replaced with something that would have to reach across a component boundary to work.

### ChartConfig

One entry per series, keyed by `dataKey` — or, where every mark comes from one series, by the values of its `nameKey`.

| Field | Type | Description |
| --- | --- | --- |
| `label` | `ReactNode` | Printed by the tooltip and the legend. |
| `color` | `string` | The series color. Use the `chart1` … `chart6` tokens, which are themed for light and dark. |
| `icon` | `ComponentType` | Rendered in the legend in place of the swatch. |

The color lives here and is read back where the mark is declared — `fill={chartConfig.revenue.color}` — so one value reaches the plot, the tooltip, and the legend.

### ChartContainer

Provides the config to the tooltip and legend bodies, sets the font and color the chart's text inherits, and measures the plot. Its single child is the chart element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `config` | `ChartConfig` | — | Series labels and colors (required). |
| `children` | `ReactElement` | — | The chart element, e.g. `<BarChart>` (required). |
| `style` | `StyleXStyles` | — | StyleX styles merged last — always win over the component's own styles. Override `aspectRatio` here. |

All native `div` props are forwarded.

### ChartTooltip

Recharts' `Tooltip`, re-exported. Give it a body with `content={<ChartTooltipContent />}`, and set `cursor` to a token so the hover marker matches the theme: `cursor={{ fill: colors.accent }}` for bars, `cursor={{ stroke: colors.border }}` for lines and areas, `cursor={false}` where none is wanted.

### ChartTooltipContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `indicator` | `'dot' \| 'line'` | `'dot'` | Swatch shape next to each row. |
| `hideLabel` | `boolean` | `false` | Omits the title. |
| `hideIndicator` | `boolean` | `false` | Omits the swatches. |
| `labelKey` | `string` | — | Config key for the title, when the axis value is not one. |
| `nameKey` | `string` | — | Config key for every row's label, when the `dataKey` is not one. |
| `formatter` | `(value, name, item, index, payload) => ReactNode` | — | Replaces a row's value. |
| `labelFormatter` | `(label, payload) => ReactNode` | — | Replaces the title. |
| `style` | `StyleXStyles` | — | StyleX styles merged last — always win over the component's own styles. |

`active`, `payload`, and `label` are injected by `ChartTooltip`; you do not pass them.

### ChartLegend

Recharts' `Legend`, re-exported. Give it a body with `content={<ChartLegendContent />}`.

### ChartLegendContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hideIcon` | `boolean` | `false` | Omits the swatches. |
| `nameKey` | `string` | — | Config key for every entry's label, when the `dataKey` is not one. |
| `style` | `StyleXStyles` | — | StyleX styles merged last — always win over the component's own styles. |

`payload` and `verticalAlign` are injected by `ChartLegend`.

### useChart

```ts
const { config } = useChart();
```

The config of the nearest `ChartContainer`, for a tooltip or legend body of your own. Throws outside one.

### Styling

`ChartContainer`, `ChartTooltipContent`, and `ChartLegendContent` accept `style` (`StyleXStyles`, merged last so caller overrides always win).

The plot itself is painted by Recharts as SVG, so its colors and geometry are props on the marks and guides rather than CSS. Tokens work there directly — they compile to `var()`, which SVG paint resolves the same way CSS does:

```tsx
<CartesianGrid vertical={false} stroke={colors.border} strokeDasharray="4 4" />
<XAxis
  dataKey="month"
  tickLine={false}
  axisLine={false}
  tickMargin={8}
  tick={{ fill: colors.mutedForeground, fontSize: fontSize.xs }}
/>
```

Sizes on a mark — `strokeWidth`, `radius`, `tickMargin`, `cornerRadius` — are plain numbers in SVG user units, not tokens.
