Chart
shadcn/ui's Recharts charts, restyled after Swift Charts: secondary axis labels, hairline grids, round markers and a glass tooltip.
@cupertinoui/chartRegistry JSON "use client"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [
{ day: "Mon", productivity: 3.2, social: 1.1, entertainment: 0.8 },
{ day: "Tue", productivity: 4.1, social: 0.9, entertainment: 0.6 },
{ day: "Wed", productivity: 3.7, social: 1.4, entertainment: 1.2 },
{ day: "Thu", productivity: 4.4, social: 0.7, entertainment: 0.9 },
{ day: "Fri", productivity: 2.9, social: 1.6, entertainment: 1.8 },
{ day: "Sat", productivity: 0.8, social: 2.1, entertainment: 2.6 },
{ day: "Sun", productivity: 0.6, social: 1.8, entertainment: 3.1 },
]
// Colors are CSS colors: here the macOS system colors behind --chart-1 to
// --chart-5.
const config = {
productivity: { label: "Productivity", color: "var(--chart-1)" },
social: { label: "Social", color: "var(--chart-3)" },
entertainment: { label: "Entertainment", color: "var(--chart-4)" },
} satisfies ChartConfig
export default function ChartDemo() {
return (
<div className="w-full max-w-md rounded-[16px] bg-mac-window p-4 shadow-[0_0_0_1px_var(--mac-grid)]">
<div className="mb-3 flex flex-col gap-0.5">
<span className="text-mac-body font-semibold">Screen Time</span>
<span className="text-mac-subheadline text-mac-label-secondary">Hours a day, this week</span>
</div>
<ChartContainer config={config} className="aspect-auto h-56 w-full">
<BarChart data={data} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>
<CartesianGrid vertical={false} strokeDasharray="2 2" />
<XAxis dataKey="day" tickLine={false} axisLine={false} tickMargin={8} />
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="productivity" stackId="time" fill="var(--color-productivity)" />
<Bar dataKey="social" stackId="time" fill="var(--color-social)" />
<Bar dataKey="entertainment" stackId="time" fill="var(--color-entertainment)" radius={[3, 3, 0, 0]} />
</BarChart>
</ChartContainer>
</div>
)
}Installation
pnpm dlx shadcn@latest add @cupertinoui/chartThe first install also adds the macOS theme. See Installation to set up the registry.
Usage
import { Bar, BarChart, XAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const config = {
hours: { label: "Hours", color: "var(--chart-1)" },
} satisfies ChartConfig
<ChartContainer config={config} className="h-48 w-full">
<BarChart data={data}>
<XAxis dataKey="day" tickLine={false} axisLine={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="hours" fill="var(--color-hours)" radius={3} />
</BarChart>
</ChartContainer>Examples
Area
Areas with a gradient fill and a line tooltip that follows the pointer, like Activity Monitor's network graph.
"use client"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [42, 38, 55, 61, 48, 72, 96, 81, 64, 70, 58, 66, 90, 112, 98, 76, 62, 68, 54, 49].map((received, index) => ({
time: `${String(9 + Math.floor(index / 4)).padStart(2, "0")}:${String((index % 4) * 15).padStart(2, "0")}`,
received,
sent: Math.round(received * 0.35 + ((index * 7) % 11)),
}))
const config = {
received: { label: "Received", color: "var(--mac-blue)" },
sent: { label: "Sent", color: "var(--mac-red)" },
} satisfies ChartConfig
// Network activity as in Activity Monitor: areas with a gradient, and a
// line tooltip that follows the pointer.
export default function ChartArea() {
return (
<div className="w-full max-w-md rounded-[16px] bg-mac-window p-4 shadow-[0_0_0_1px_var(--mac-grid)]">
<div className="mb-3 flex items-baseline justify-between">
<span className="text-mac-body font-semibold">Network</span>
<span className="text-mac-subheadline text-mac-label-secondary">KB/s</span>
</div>
<ChartContainer config={config} className="aspect-auto h-48 w-full">
<AreaChart data={data} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
<defs>
{(["received", "sent"] as const).map((key) => (
<linearGradient key={key} id={`fill-${key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={`var(--color-${key})`} stopOpacity={0.4} />
<stop offset="100%" stopColor={`var(--color-${key})`} stopOpacity={0.02} />
</linearGradient>
))}
</defs>
<CartesianGrid vertical={false} strokeDasharray="2 2" />
<XAxis dataKey="time" tickLine={false} axisLine={false} tickMargin={8} interval={3} />
<YAxis tickLine={false} axisLine={false} width={28} tickCount={4} />
<ChartTooltip cursor={{ strokeDasharray: "2 2" }} content={<ChartTooltipContent indicator="line" />} />
<Area dataKey="received" type="monotone" stroke="var(--color-received)" strokeWidth={1.5} fill="url(#fill-received)" />
<Area dataKey="sent" type="monotone" stroke="var(--color-sent)" strokeWidth={1.5} fill="url(#fill-sent)" />
</AreaChart>
</ChartContainer>
</div>
)
}Line
Lines with a legend; theme gives a series its own color in light and dark.
"use client"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
const data = [
{ month: "May", iphone: 98, mac: 100 },
{ month: "Jun", iphone: 97, mac: 100 },
{ month: "Jul", iphone: 96, mac: 99 },
{ month: "Aug", iphone: 95, mac: 99 },
{ month: "Sep", iphone: 93, mac: 98 },
{ month: "Oct", iphone: 92, mac: 98 },
]
// `theme` gives a series one color for light and one for dark.
const config = {
mac: { label: "MacBook Pro", theme: { light: "#1d1d1f", dark: "#f5f5f7" } },
iphone: { label: "iPhone", color: "var(--mac-green)" },
} satisfies ChartConfig
export default function ChartLine() {
return (
<div className="w-full max-w-md rounded-[16px] bg-mac-window p-4 shadow-[0_0_0_1px_var(--mac-grid)]">
<div className="mb-3 flex flex-col gap-0.5">
<span className="text-mac-body font-semibold">Battery Health</span>
<span className="text-mac-subheadline text-mac-label-secondary">Maximum capacity, last 6 months</span>
</div>
<ChartContainer config={config} className="aspect-auto h-48 w-full">
<LineChart data={data} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
<CartesianGrid vertical={false} strokeDasharray="2 2" />
<XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} width={44} domain={[90, 100]} tickFormatter={(value: number) => `${value} %`} />
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Line dataKey="mac" type="monotone" stroke="var(--color-mac)" strokeWidth={2} dot={false} />
<Line dataKey="iphone" type="monotone" stroke="var(--color-iphone)" strokeWidth={2} dot={{ r: 3, fill: "var(--color-iphone)" }} />
</LineChart>
</ChartContainer>
</div>
)
}Battery Level
Hourly bars as in System Settings, brighter while charging, with a custom tooltip row.
"use client"
import { Bar, BarChart, CartesianGrid, Cell, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
// The battery level each hour of the last day, from 19:00: it charged
// until 07:00.
const battery = [62, 58, 54, 51, 47, 44, 40, 37, 34, 52, 74, 92, 100, 100, 96, 91, 86, 80, 75, 69, 66, 61, 57, 53].map(
(level, index) => ({ hour: `${String((19 + index) % 24).padStart(2, "0")}:00`, level, charging: index >= 9 && index <= 12 })
)
const batteryConfig = {
level: { label: "Battery", color: "var(--mac-green)" },
} satisfies ChartConfig
// Battery Level as in System Settings: an hourly bar for the last day,
// brighter while charging, with its own tooltip row.
export default function ChartBattery() {
return (
<div className="flex w-full max-w-sm flex-col rounded-[18px] bg-mac-window p-4 text-mac-body text-mac-label shadow-[0_0_0_1px_var(--mac-grid),0_1px_3px_rgb(0_0_0/0.04)]">
<div className="mb-3 flex flex-col gap-0.5">
<span className="font-semibold">Battery Level</span>
<span className="text-mac-subheadline text-mac-label-secondary">Last 24 hours</span>
</div>
<ChartContainer config={batteryConfig} className="aspect-auto h-28 w-full">
<BarChart data={battery} margin={{ top: 4, right: 0, bottom: 0, left: 0 }} barCategoryGap={2}>
<CartesianGrid vertical={false} strokeDasharray="2 2" />
<XAxis
dataKey="hour"
tickLine={false}
axisLine={false}
interval={5}
tickMargin={6}
tickFormatter={(hour: string) => hour.slice(0, 2)}
/>
<YAxis hide domain={[0, 100]} />
<ChartTooltip
cursor={false}
content={<ChartTooltipContent formatter={(value, _name, item) => (
<div className="flex flex-1 items-center justify-between gap-3">
<span className="text-mac-label-secondary">{item.payload.charging ? "Charging" : "Battery"}</span>
<span className="font-semibold text-mac-label tabular-nums">{value} %</span>
</div>
)} />}
/>
<Bar dataKey="level" radius={[2, 2, 0, 0]}>
{battery.map((entry) => (
<Cell key={entry.hour} fill="var(--color-level)" fillOpacity={entry.charging ? 1 : 0.6} />
))}
</Bar>
</BarChart>
</ChartContainer>
<div className="mt-3 grid grid-cols-2 gap-2">
{[
["Last charged", "Today, 07:12"],
["Screen on", "4 h 12 min"],
].map(([label, value]) => (
<div key={label} className="rounded-[10px] bg-mac-fill-quaternary p-2.5">
<div className="text-[10px] font-semibold tracking-wide text-mac-label-secondary uppercase">{label}</div>
<div className="mt-0.5 font-semibold">{value}</div>
</div>
))}
</div>
</div>
)
}CPU Load
Stacked areas as in Activity Monitor, with a line tooltip and a legend.
"use client"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
// CPU load over the last minute, every three seconds.
const cpu = [22, 26, 24, 31, 28, 35, 30, 42, 38, 33, 29, 36, 45, 40, 34, 30, 27, 32, 38, 33].map((user, index) => ({
second: index * 3,
user,
system: [8, 9, 8, 11, 10, 12, 10, 15, 13, 11, 9, 12, 16, 14, 12, 10, 9, 11, 13, 11][index],
}))
const cpuConfig = {
system: { label: "System", color: "var(--mac-red)" },
user: { label: "User", color: "var(--mac-blue)" },
} satisfies ChartConfig
// CPU load as in Activity Monitor: System and User stacked, with the
// current total in the header.
export default function ChartCpu() {
const last = cpu.at(-1)!
return (
<div className="flex w-full max-w-sm flex-col rounded-[18px] bg-mac-window p-4 text-mac-body text-mac-label shadow-[0_0_0_1px_var(--mac-grid),0_1px_3px_rgb(0_0_0/0.04)]">
<div className="mb-3 flex items-start justify-between gap-2">
<div className="flex flex-col gap-0.5">
<span className="font-semibold">CPU Load</span>
<span className="text-mac-subheadline text-mac-label-secondary">Activity Monitor</span>
</div>
<span className="text-[17px] font-semibold tabular-nums">{last.user + last.system} %</span>
</div>
<ChartContainer config={cpuConfig} className="aspect-auto h-32 w-full">
<AreaChart data={cpu} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>
<CartesianGrid vertical={false} strokeDasharray="2 2" />
<XAxis dataKey="second" hide />
<YAxis hide domain={[0, 70]} />
<ChartTooltip
cursor={{ strokeDasharray: "2 2" }}
content={<ChartTooltipContent indicator="line" labelFormatter={(_, payload) => `${60 - Number(payload[0]?.payload.second ?? 0)} s ago`} />}
/>
<Area dataKey="system" type="monotone" stackId="cpu" stroke="var(--color-system)" strokeWidth={1.5} fill="var(--color-system)" fillOpacity={0.3} />
<Area dataKey="user" type="monotone" stackId="cpu" stroke="var(--color-user)" strokeWidth={1.5} fill="var(--color-user)" fillOpacity={0.25} />
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</div>
)
}API Reference
Every part forwards the props of the underlying element. The props below are the ones CupertinoUI adds or changes.
ChartContainer
Wraps a Recharts chart, makes it responsive and applies the styles.
| Prop | Type | Description |
|---|---|---|
config | ChartConfig | The series by data key: { label, color } or { label, theme: { light, dark } }, and an optional icon for the legend and tooltip. |
initialDimension | { width: number; height: number }Default: { width: 320, height: 200 } | The size used before the container is measured, e.g. when rendering on the server. |
className | string | Extra classes, merged with the component's own. |
ChartTooltipContent
The content for ChartTooltip.
| Prop | Type | Description |
|---|---|---|
indicator | "dot" | "line" | "dashed"Default: "dot" | The marker before each series. |
hideLabel / hideIndicator | booleanDefault: false | Hide the label at the top, or the markers. |
labelKey / nameKey | string | Config keys for the label and the series names, when they aren't the data keys. |
labelFormatter / formatter | function | Format the label, or render a series row yourself. |
className | string | Extra classes, merged with the component's own. |
ChartLegendContent
The content for ChartLegend.
| Prop | Type | Description |
|---|---|---|
hideIcon | booleanDefault: false | Show round markers even when the config has icons. |
nameKey | string | The config key for the series names. |
className | string | Extra classes, merged with the component's own. |