Skip to content
Esc
navigateopen⌘Jpreview
On this page

Chart

Charts built on Recharts: a config-driven container plus tooltip and legend bodies, styled with the design tokens.

'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

npx @madeui/cli add chart

Installs recharts alongside the component.

Usage

import {
  ChartContainer,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  useChart,
  type ChartConfig,
} from '@/components/ui/chart';
'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

<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.

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.

'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.

'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.

'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.

'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.

'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.

'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.

'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.

Active users

1,690

Up 43% over 14 days

'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. The chart element, marks, axes, grids, and cursors are Recharts’ own components, composed directly and documented in the Recharts API reference; 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 chart1chart6 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

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:

<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.

Was this page helpful?