Radar Skill Chart
Multi-axis radar chart: a drawing animation on reveal, values on vertex hover, and two series overlaid for comparison.
A multi-axis radar chart drawn in pure SVG: on reveal the data polygon expands out from the center while the outline grows along its edges, hovering a vertex floats a tooltip with that axis's value, and a second data series can be overlaid semi-transparently for comparison — which suits skill assessments, product comparisons, and similar storytelling.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add radar-skill-chart.enRadar Skill Chart is a React radar chart (spider chart) component built with SVG and Motion (motion/react), with no charting library involved. On entry the data polygon expands from the center while its outline draws along the edges; you can overlay a second series for comparison, and hovering or focusing a vertex shows a tooltip.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<RadarSkillChart/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- Skill breakdowns on a portfolio or resume page
- Comparing a candidate's strengths with a role's requirements in a hiring dashboard
- Product comparison pages showing two plans across several dimensions
- Course or assessment results showing scores per subject
- Not a good fit: with only one or two dimensions, or when you need a trend over time, a radar chart is harder to read; use the bar or line charts in Animated Charts
Installation
npx webberui@latest add https://webberui.com/r/radar-skill-chart.en.jsonOr, once registries are configured in components.json, install it as @webberui/radar-skill-chart.
Usage
import { RadarSkillChart } from "@/components/ui/radar-skill-chart";
<RadarSkillChart
axes={[
{ label: "React", value: 92 },
{ label: "TypeScript", value: 85 },
{ label: "Testing", value: 62 },
{ label: "Collaboration", value: 88 },
]}
series2={[60, 48, 30, 72]}
seriesLabel="This year"
series2Label="Last year"
/>Examples
A 1 to 5 rating
Set max to 5 and rings to 5 so each ring matches one point.
import { RadarSkillChart } from "@/components/ui/radar-skill-chart";
<RadarSkillChart
max={5}
rings={5}
axes={[
{ label: "Ease of use", value: 4 },
{ label: "Performance", value: 3 },
{ label: "Docs", value: 5 },
{ label: "Support", value: 4 },
{ label: "Pricing", value: 2 },
]}
/>;Candidate versus role requirements
Put the candidate's scores in the main series and the role's thresholds in series2, in the same order as axes; switch to brand colors with color and color2.
<RadarSkillChart
axes={[
{ label: "Frontend", value: 88 },
{ label: "Backend", value: 54 },
{ label: "System design", value: 70 },
{ label: "Communication", value: 82 },
{ label: "Leadership", value: 45 },
{ label: "Product sense", value: 76 },
]}
series2={[80, 60, 65, 70, 50, 60]}
seriesLabel="Candidate"
series2Label="Role"
color="#0ea5e9"
color2="#a3a3a3"
size={360}
/>Load data from an API
It is a client component, so fetch in a Server Component and pass the data straight in as props.
// app/team/[id]/page.tsx
import { RadarSkillChart } from "@/components/ui/radar-skill-chart";
export default async function MemberPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const res = await fetch(`https://api.example.com/members/${id}/skills`);
const skills: { name: string; score: number }[] = await res.json();
return (
<RadarSkillChart
axes={skills.map((s) => ({ label: s.name, value: s.score }))}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
axes | RadarAxisDatum[] | — | Per-axis data { label, value }, at least 3 axes, with the primary series values embedded in it; long labels wrap at spaces, and if labels still reach past the chart the whole chart scales down so they stay within size |
series2 | number[] | — | The second set of values (overlaid semi-transparently for comparison); the order must line up with axes |
seriesLabel | string | "Series A" | Name of the primary series, used in the legend and the tooltip when there are two series (the English source includes this default, so pass this prop to localize it) |
series2Label | string | "Series B" | Name of the second series (the English source includes this default, so pass this prop to localize it) |
max | number | 100 | Value ceiling, corresponding to the outermost ring |
size | number | 320 | Rendered size (px, width = height) |
rings | number | 4 | Number of concentric grid rings |
color | string | "#6366f1" | Primary series color (any valid CSS color value) |
color2 | string | "#f59e0b" | Second series color |
className | string | — | Forwarded to the outermost container |
Accessibility
- The SVG has a descriptive
aria-labelthat announces every axis label and every series value in full - Each vertex hit area is keyboard focusable (Tab moves through them in order) and shows the tooltip on focus just as on hover, each carrying its own
aria-labelannouncing the value - When the user has "reduce motion" enabled at the system level, the expansion and stroke-drawing animations are skipped and the finished shape is shown directly
- Hovering also darkens the matching axis line and label, giving an extra visual cue for lining things up
FAQ
Does it work with the Next.js App Router?
Yes. The file already starts with "use client", so import it from a Server Component page and pass data in as props.
Why is the chart not showing?
A radar chart needs at least 3 axes to have an area, so the component returns null when axes has fewer than 3 items. Values above max or below 0 are clamped to the 0 to max range.
Does the animation replay when data changes?
No. The entrance expansion runs once on mount; after that, changes to axes or series2 recompute the polygon coordinates directly to the new values.
How do I change colors and size?
color and color2 take any CSS color value for the two series; size sets the rendered width and height in px, scaling the internal viewBox (which grows when axis labels are long, so the chart shrinks to keep them inside); rings sets the number of grid rings.
Can screen readers read the values?
Yes. The SVG aria-label lists every dimension's values, and each vertex hit area can be focused with Tab and announces that axis. With the OS "reduce motion" setting on, the entrance animation is skipped.
Guess Gap Reveal
Guess-the-gap reveal — drag a slider or draw a trend line to guess the answer first, then let go and watch the real data animate in, with a color block marking the gap between "your guess" and the truth alongside the distribution of other readers' guesses.
Bump Rank Chart
Multi-period rank line chart: curve stroke-drawing animation, with hover highlighting a single entry's path.