Skill radar
The product’s signature 8-axis data visualization. Ember Orange carries the data shape; gray category rings mark skill tier; dashed brand ring is the 70% readiness target. Tier-colored numeric values render outside the polygon. Skill identity lives in the axis label, never the hue.
Preview
FoundationStructureDepthDelivery70% Target
Source
apps/docs/app/recipes/radar/recipe.tsxconst AXES = [
{ x: 0, y: -94, l: 75 },
{ x: 37.5, y: -37.5, l: 75 },
{ x: 74, y: 0, l: 60 },
{ x: 67.9, y: 67.9, l: 60 },
{ x: 0, y: 67, l: 45 },
{ x: -59.4, y: 59.4, l: 45 },
{ x: -91, y: 0, l: 35 },
{ x: -54.4, y: -54.4, l: 35 },
];
const CATEGORIES = [
{ label: 'Foundation', l: 75 },
{ label: 'Structure', l: 60 },
{ label: 'Depth', l: 45 },
{ label: 'Delivery', l: 35 },
];
export default function RadarRecipe() {
return (
<div className="border-stroke bg-surface-2 rounded-2xl border p-6">
<header className="mb-2 flex items-baseline justify-between">
<span className="tracking-label text-ink-3 text-label font-mono font-bold uppercase">
Skill radar · you vs. 70% target
</span>
<span className="text-ink-3 font-mono text-3xs tracking-[0.06em] uppercase">
BRAND · DATA · GRAY · CATEGORY
</span>
</header>
<svg viewBox="0 0 360 320" width="100%" height="280" aria-label="Skill radar chart">
<g transform="translate(180,158)">
{/* concentric category rings */}
<g fill="none" stroke="var(--chart-gridline)">
<circle r="120" strokeWidth="1" opacity="0.55" />
<circle r="90" strokeWidth="1" opacity="0.35" />
<circle r="60" strokeWidth="1" opacity="0.25" />
<circle r="30" strokeWidth="1" opacity="0.18" />
</g>
{/* 8 axis spokes */}
<g stroke="var(--text-ink-3)" strokeWidth="1" opacity="0.18">
<line x1="0" y1="0" x2="0" y2="-120" />
<line x1="0" y1="0" x2="84.85" y2="-84.85" />
<line x1="0" y1="0" x2="120" y2="0" />
<line x1="0" y1="0" x2="84.85" y2="84.85" />
<line x1="0" y1="0" x2="0" y2="120" />
<line x1="0" y1="0" x2="-84.85" y2="84.85" />
<line x1="0" y1="0" x2="-120" y2="0" />
<line x1="0" y1="0" x2="-84.85" y2="-84.85" />
</g>
{/* 0.7 readiness target ring */}
<circle
r="84"
fill="none"
stroke="var(--color-brand)"
strokeWidth="1.25"
strokeDasharray="4 4"
opacity="0.7"
/>
<text
x="0"
y="-87"
textAnchor="middle"
fontFamily="ui-monospace, monospace"
style={{ fontSize: 'var(--text-3xs)' }}
fontWeight="700"
fill="var(--color-brand)"
letterSpacing="0.08em"
opacity="0.85"
>
0.7 TARGET
</text>
{/* data polygon */}
<polygon
points="0,-94 37.5,-37.5 74,0 67.9,67.9 0,67 -59.4,59.4 -91,0 -54.4,-54.4"
fill="var(--color-brand-overlay-15)"
stroke="var(--color-brand)"
strokeWidth="1.75"
/>
{/* per-axis nodes: gray category ring + brand-fill dot */}
{AXES.map(({ x, y, l }, i) => (
<g key={i}>
<circle
cx={x}
cy={y}
r="6.5"
fill="var(--bg-surface-2)"
stroke={`oklch(${l}% 0 0)`}
strokeWidth="1.6"
/>
<circle cx={x} cy={y} r="3.2" fill="var(--color-brand)" />
</g>
))}
{/* axis labels */}
<g
fontFamily="ui-monospace, monospace"
style={{ fontSize: 'var(--text-3xs)' }}
fontWeight="700"
letterSpacing="0.08em"
fill="var(--text-ink-2)"
>
<text x="0" y="-138" textAnchor="middle">REQUIREMENTS</text>
<text x="98" y="-98" textAnchor="middle">SCALE EST.</text>
<text x="138" y="3" textAnchor="start">API DESIGN</text>
<text x="98" y="105" textAnchor="middle">HLD</text>
<text x="0" y="142" textAnchor="middle">BOTTLENECKS</text>
<text x="-98" y="105" textAnchor="middle">SCALING</text>
<text x="-138" y="3" textAnchor="end">TRADE-OFFS</text>
<text x="-98" y="-98" textAnchor="middle">COMMS</text>
</g>
{/* tier-colored score values outside the polygon */}
<g fontFamily="ui-monospace, monospace" style={{ fontSize: 'var(--text-3xs)' }} fontWeight="700">
<text x="0" y="-126" textAnchor="middle" fill="var(--color-score-passing-text)">78%</text>
<text x="98" y="-86" textAnchor="middle" fill="var(--color-score-below-text)">44%</text>
<text x="138" y="15" textAnchor="start" fill="var(--color-score-approaching-text)">62%</text>
<text x="98" y="117" textAnchor="middle" fill="var(--color-score-passing-text)">80%</text>
<text x="0" y="154" textAnchor="middle" fill="var(--color-score-below-text)">56%</text>
<text x="-98" y="117" textAnchor="middle" fill="var(--color-score-passing-text)">70%</text>
<text x="-138" y="15" textAnchor="end" fill="var(--color-score-passing-text)">76%</text>
<text x="-98" y="-86" textAnchor="middle" fill="var(--color-score-approaching-text)">64%</text>
</g>
</g>
</svg>
{/* category legend */}
<div className="border-stroke mt-4 flex flex-wrap items-center gap-4 border-t border-dashed pt-4">
{CATEGORIES.map(({ label, l }) => (
<span
key={label}
className="tracking-label text-ink-3 inline-flex items-center gap-1.5 font-mono text-3xs font-bold uppercase"
>
<span
aria-hidden
className="bg-surface-2 inline-block h-2 w-2 rounded-full"
style={{ border: `1.5px solid oklch(${l}% 0 0)` }}
/>
{label}
</span>
))}
<span className="tracking-label text-brand ml-auto inline-flex items-center gap-1.5 font-mono text-3xs font-bold uppercase">
<span
aria-hidden
className="inline-block h-0 w-3.5"
style={{ borderTop: '1.25px dashed var(--color-brand)' }}
/>
70% Target
</span>
</div>
</div>
);
}
const AXES = [
{ x: 0, y: -94, l: 75 },
{ x: 37.5, y: -37.5, l: 75 },
{ x: 74, y: 0, l: 60 },
{ x: 67.9, y: 67.9, l: 60 },
{ x: 0, y: 67, l: 45 },
{ x: -59.4, y: 59.4, l: 45 },
{ x: -91, y: 0, l: 35 },
{ x: -54.4, y: -54.4, l: 35 },
];
const CATEGORIES = [
{ label: 'Foundation', l: 75 },
{ label: 'Structure', l: 60 },
{ label: 'Depth', l: 45 },
{ label: 'Delivery', l: 35 },
];
export default function RadarRecipe() {
return (
<div className="border-stroke bg-surface-2 rounded-2xl border p-6">
<header className="mb-2 flex items-baseline justify-between">
<span className="tracking-label text-ink-3 text-label font-mono font-bold uppercase">
Skill radar · you vs. 70% target
</span>
<span className="text-ink-3 font-mono text-3xs tracking-[0.06em] uppercase">
BRAND · DATA · GRAY · CATEGORY
</span>
</header>
<svg viewBox="0 0 360 320" width="100%" height="280" aria-label="Skill radar chart">
<g transform="translate(180,158)">
{/* concentric category rings */}
<g fill="none" stroke="var(--chart-gridline)">
<circle r="120" strokeWidth="1" opacity="0.55" />
<circle r="90" strokeWidth="1" opacity="0.35" />
<circle r="60" strokeWidth="1" opacity="0.25" />
<circle r="30" strokeWidth="1" opacity="0.18" />
</g>
{/* 8 axis spokes */}
<g stroke="var(--text-ink-3)" strokeWidth="1" opacity="0.18">
<line x1="0" y1="0" x2="0" y2="-120" />
<line x1="0" y1="0" x2="84.85" y2="-84.85" />
<line x1="0" y1="0" x2="120" y2="0" />
<line x1="0" y1="0" x2="84.85" y2="84.85" />
<line x1="0" y1="0" x2="0" y2="120" />
<line x1="0" y1="0" x2="-84.85" y2="84.85" />
<line x1="0" y1="0" x2="-120" y2="0" />
<line x1="0" y1="0" x2="-84.85" y2="-84.85" />
</g>
{/* 0.7 readiness target ring */}
<circle
r="84"
fill="none"
stroke="var(--color-brand)"
strokeWidth="1.25"
strokeDasharray="4 4"
opacity="0.7"
/>
<text
x="0"
y="-87"
textAnchor="middle"
fontFamily="ui-monospace, monospace"
style={{ fontSize: 'var(--text-3xs)' }}
fontWeight="700"
fill="var(--color-brand)"
letterSpacing="0.08em"
opacity="0.85"
>
0.7 TARGET
</text>
{/* data polygon */}
<polygon
points="0,-94 37.5,-37.5 74,0 67.9,67.9 0,67 -59.4,59.4 -91,0 -54.4,-54.4"
fill="var(--color-brand-overlay-15)"
stroke="var(--color-brand)"
strokeWidth="1.75"
/>
{/* per-axis nodes: gray category ring + brand-fill dot */}
{AXES.map(({ x, y, l }, i) => (
<g key={i}>
<circle
cx={x}
cy={y}
r="6.5"
fill="var(--bg-surface-2)"
stroke={`oklch(${l}% 0 0)`}
strokeWidth="1.6"
/>
<circle cx={x} cy={y} r="3.2" fill="var(--color-brand)" />
</g>
))}
{/* axis labels */}
<g
fontFamily="ui-monospace, monospace"
style={{ fontSize: 'var(--text-3xs)' }}
fontWeight="700"
letterSpacing="0.08em"
fill="var(--text-ink-2)"
>
<text x="0" y="-138" textAnchor="middle">REQUIREMENTS</text>
<text x="98" y="-98" textAnchor="middle">SCALE EST.</text>
<text x="138" y="3" textAnchor="start">API DESIGN</text>
<text x="98" y="105" textAnchor="middle">HLD</text>
<text x="0" y="142" textAnchor="middle">BOTTLENECKS</text>
<text x="-98" y="105" textAnchor="middle">SCALING</text>
<text x="-138" y="3" textAnchor="end">TRADE-OFFS</text>
<text x="-98" y="-98" textAnchor="middle">COMMS</text>
</g>
{/* tier-colored score values outside the polygon */}
<g fontFamily="ui-monospace, monospace" style={{ fontSize: 'var(--text-3xs)' }} fontWeight="700">
<text x="0" y="-126" textAnchor="middle" fill="var(--color-score-passing-text)">78%</text>
<text x="98" y="-86" textAnchor="middle" fill="var(--color-score-below-text)">44%</text>
<text x="138" y="15" textAnchor="start" fill="var(--color-score-approaching-text)">62%</text>
<text x="98" y="117" textAnchor="middle" fill="var(--color-score-passing-text)">80%</text>
<text x="0" y="154" textAnchor="middle" fill="var(--color-score-below-text)">56%</text>
<text x="-98" y="117" textAnchor="middle" fill="var(--color-score-passing-text)">70%</text>
<text x="-138" y="15" textAnchor="end" fill="var(--color-score-passing-text)">76%</text>
<text x="-98" y="-86" textAnchor="middle" fill="var(--color-score-approaching-text)">64%</text>
</g>
</g>
</svg>
{/* category legend */}
<div className="border-stroke mt-4 flex flex-wrap items-center gap-4 border-t border-dashed pt-4">
{CATEGORIES.map(({ label, l }) => (
<span
key={label}
className="tracking-label text-ink-3 inline-flex items-center gap-1.5 font-mono text-3xs font-bold uppercase"
>
<span
aria-hidden
className="bg-surface-2 inline-block h-2 w-2 rounded-full"
style={{ border: `1.5px solid oklch(${l}% 0 0)` }}
/>
{label}
</span>
))}
<span className="tracking-label text-brand ml-auto inline-flex items-center gap-1.5 font-mono text-3xs font-bold uppercase">
<span
aria-hidden
className="inline-block h-0 w-3.5"
style={{ borderTop: '1.25px dashed var(--color-brand)' }}
/>
70% Target
</span>
</div>
</div>
);
}