radial-chart-11
Fine radial progress rings for daily targets: three thin blue rings, each with its own label and exact value, over a ruled caption.
pnpm dlx shadcn add @01orbital/radial-chart-11"use client"
import {
Label,
PolarAngleAxis,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts"
import { Eyebrow } from "@/components/eyebrow/eyebrow"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { ChartContainer, type ChartConfig } from "@/components/ui/chart"
import { Separator } from "@/components/ui/separator"
const COPY = {
title: "Daily targets",
description: "Today’s progress",
} as const
const GOALS = [
{
key: "focus",
label: "Focus",
value: 3,
target: 4,
unit: "hours",
color: "var(--art-accent)",
},
{
key: "tasks",
label: "Tasks",
value: 8,
target: 10,
unit: "tasks",
color: "var(--art-accent)",
},
{
key: "breaks",
label: "Breaks",
value: 3,
target: 5,
unit: "breaks",
color: "var(--art-accent)",
},
] as const
const CHART_CONFIG = Object.fromEntries(
GOALS.map((goal) => [goal.key, { label: goal.label, color: goal.color }])
) satisfies ChartConfig
/** Three small progress rings, each paired with its own label and exact target. */
export default function RadialChart11() {
return (
<Card className="@container/targets mx-auto w-full max-w-[26rem] gap-8 [--card-spacing:24px]">
<CardHeader>
<CardTitle>
<h2>{COPY.title}</h2>
</CardTitle>
<CardDescription>{COPY.description}</CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-6 @[16rem]/targets:grid-cols-3 @[16rem]/targets:gap-3">
{GOALS.map((goal) => (
<figure key={goal.key} className="min-w-0 space-y-3 text-center">
<ChartContainer
config={CHART_CONFIG}
role="img"
aria-label={`${goal.label}: ${goal.value} of ${goal.target} ${goal.unit}`}
className="mx-auto aspect-square w-full max-w-28"
>
<RadialBarChart
accessibilityLayer={false}
data={[{ value: goal.value, fill: `var(--color-${goal.key})` }]}
startAngle={90}
endAngle={-270}
innerRadius="90%"
outerRadius="100%"
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
>
<PolarAngleAxis
type="number"
domain={[0, goal.target]}
tick={false}
/>
<RadialBar dataKey="value" background cornerRadius={0} />
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label
content={({ viewBox }) => {
if (!viewBox || !("cx" in viewBox)) return null
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
fontSize={16}
className="fill-foreground tabular-nums"
>
{Math.round((goal.value / goal.target) * 100)}%
</text>
)
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<figcaption className="space-y-1 text-sm">
<p className="font-medium">{goal.label}</p>
<p className="text-xs/5 text-muted-foreground tabular-nums">
{goal.value} / {goal.target} {goal.unit}
</p>
</figcaption>
</figure>
))}
</CardContent>
<CardFooter className="grid gap-3">
<Separator />
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
<Eyebrow variant="mono" render={<span />}>
Today
</Eyebrow>
<span className="text-sm/7 tabular-nums">{GOALS.length} goals</span>
</div>
</CardFooter>
</Card>
)
}
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
// default is a quiet 14px line, cased as written; icon puts a 16px icon in
// front of it; mono is a 12px uppercase mono label for spec-sheet designs;
// caps is the same size in a sans face. tone picks the ink: muted (the
// default) or the surrounding text color, for a label over art or on a dark
// island where muted-foreground is too quiet.
const eyebrowVariants = cva("", {
variants: {
/**
* The label style. `default` (the default) is a 14px line at weight 500,
* cased as written. `icon` is the same line with a 16px icon, placed
* first among the children, 12px before the words. `mono` is a 12px
* uppercase mono label at weight 500 with 0.12em letter spacing. `caps`
* is the same label in the sans face, for a brand and series label such
* as "Studio Neri / Clay school". The color comes from `tone`.
*/
variant: {
default: "text-sm/5 font-medium",
icon: "flex items-center gap-3 text-sm/5 font-medium [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
mono: "font-mono text-xs/5 font-medium tracking-[0.12em] uppercase",
caps: "text-xs/5 font-medium tracking-[0.12em] uppercase",
},
/**
* The ink. `muted` (the default) is `muted-foreground`. `ink` leaves the
* color to the surrounding text, for a label over art, on a dark island
* or anywhere the muted grey is too quiet to read.
*/
tone: {
muted: "text-muted-foreground",
ink: "",
},
},
defaultVariants: { variant: "default", tone: "muted" },
})
/**
* A short label set above a heading, such as a section name, rendered as a
* `<p>`. Leave 8px between it and the heading. Pass `render` to use another
* element.
*
* `default` is a 14px line at weight 500, cased as written. `icon` adds a
* 16px icon before the words: put the icon first, with `aria-hidden`. `mono`
* is a 12px uppercase mono label with 0.12em letter spacing; `caps` the same
* in the sans face, for a brand and series name such as "Studio Neri / Clay
* school". `tone="muted"` (the default) sets it in `muted-foreground`;
* `tone="ink"` takes the surrounding text color, for a label over art or on
* a dark island where the grey is too quiet.
*/
function Eyebrow({
className,
variant = "default",
tone = "muted",
render,
...props
}: useRender.ComponentProps<"p"> & VariantProps<typeof eyebrowVariants>) {
return useRender({
defaultTagName: "p",
render,
state: { slot: "eyebrow", variant, tone },
props: mergeProps<"p">(
{ className: cn(eyebrowVariants({ variant, tone }), className) },
props
),
})
}
export { Eyebrow, eyebrowVariants }
Dependencies
npm: recharts@3.8.0. The CLI installs them for you.
Registry pieces it installs with it: