@visx/shape
@visx/shape is the low-level SVG geometry layer of Airbnb's visx visualization toolkit. It turns data, accessors, scales, and curve choices into React elements for bars, lines, areas, pies, stacks, grouped bars, polygons, and tree links. It deliberately is not a finished charting product: you compose its primitives with scales, axes, responsive measurement, events, tooltips, animation, and accessible descriptions chosen for your application.
One of the best foundations for bespoke React visualizations when your team wants to own the chart rather than configure one. It is the wrong first install for ordinary dashboards whose real requirement is a ready-made responsive chart with axes, tooltips, and accessible interaction.
Use it if
- You need a custom React or Preact chart whose SVG markup, styling, interaction, and accessibility must remain under your control
- You understand scales and SVG geometry and want D3's shape calculations without D3 selections mutating React-owned DOM
- You are building reusable visualization components for a design system rather than dropping in a fixed dashboard chart
- You want to install only selected visx packages and tree-shake unused shape exports
- You want a complete line, bar, or pie chart from one component; @visx/shape does not supply axes, legends, tooltips, responsive sizing, data loading, themes, or chart-level accessibility
- Your team is not comfortable choosing scale domains, SVG coordinate ranges, margins, accessors, and zero baselines; the README's basic bar example makes all of those decisions in application code
- You need built-in transitions; the visx FAQ says animation is intentionally not baked in and expects you to bring a React animation library
- Your application is on React 16 or 17; visx 4 declares React 18 or 19 and matching `@types/react` as peer dependencies, while older React apps must stay on visx 3
- You want Canvas or WebGL rendering for very large datasets; these components render SVG elements, so thousands of marks can become a browser and React reconciliation problem
Setup reality
Install `@visx/shape`, React 18 or 19, and a matching `@types/react` package for TypeScript. There is no native build or required stylesheet. The package publishes CommonJS and ESM builds, bundled declarations, and `sideEffects: false`, but a useful chart normally adds other packages: `@visx/scale` for domains and ranges, `@visx/group` for margin transforms, `@visx/axis` for ticks, `@visx/responsive` for container measurement, and `@visx/tooltip` or `@visx/event` for interaction. Keep all `@visx/*` packages on the same v4 major; the v4 migration guide explicitly tells monorepo users to upgrade them together. Deep imports such as `@visx/shape/lib/shapes/Bar` are no longer the supported surface, so import from the package root. The real setup cost is chart math. SVG y coordinates increase downward, meaning a vertical scale commonly uses `[innerHeight, 0]`, and bar height is `innerHeight - yScale(value)`. Band scales may return `undefined`, so TypeScript forces you to handle that possibility. You also own invalid and missing data through `defined`, stable React keys, margins, clipping, resize behavior, event hit targets, labels, color contrast, keyboard access, and screen-reader summaries. Shape components pass ordinary SVG props through, which is flexible but makes it easy to create a pretty picture with no useful semantics. For animation, install and integrate your own React animation system. For Preact, alias React and React DOM to `preact/compat` and satisfy the declared peer range as the README instructs.
Patterns
Render a basic vertical bar chartdraw-bar-chart
import { Bar } from '@visx/shape';
import { scaleBand, scaleLinear } from '@visx/scale';
const x = scaleBand({ domain: data.map((d) => d.label), range: [0, width], padding: 0.2 });
const y = scaleLinear({ domain: [0, Math.max(...data.map((d) => d.value))], range: [height, 0] });
const bars = data.map((d) => {
const barX = x(d.label) ?? 0;
const barY = y(d.value);
return <Bar key={d.label} x={barX} y={barY} width={x.bandwidth()} height={height - barY} fill="#2563eb" />;
});SVG y grows downward. Map the numeric domain to `[height, 0]`, then subtract the scaled y position from the inner height.
Round only the tops of positive barsdraw-rounded-bars
import { BarRounded } from '@visx/shape';
<BarRounded
x={barX}
y={barY}
width={barWidth}
height={barHeight}
radius={6}
top
fill="#0f766e"
/>`BarRounded` renders a path rather than a rect. Its radius is clamped to half the shorter side, and corner flags determine which corners change.
Generate a curved line pathdraw-line-chart
import { LinePath } from '@visx/shape';
import { curveMonotoneX } from '@visx/curve';
<LinePath
data={points}
x={(d) => xScale(d.date)}
y={(d) => yScale(d.value)}
curve={curveMonotoneX}
fill="none"
stroke="#7c3aed"
strokeWidth={2}
/>Monotone interpolation avoids x-axis reversals but still changes the path between observations; use a linear curve when interpolation would imply unsupported precision.
Break a line around missing valuesskip-missing-points
<LinePath
data={points}
defined={(d) => d.value != null}
x={(d) => xScale(d.date)}
y={(d) => yScale(d.value ?? 0)}
stroke="currentColor"
fill="none"
/>The `defined` accessor creates gaps. Filtering missing rows instead would connect observations across the gap and can misrepresent the series.
Fill an area down to the zero baselinedraw-area-chart
import { AreaClosed } from '@visx/shape';
<AreaClosed
data={points}
x={(d) => xScale(d.date)}
y={(d) => yScale(d.value)}
yScale={yScale}
fill="rgba(37, 99, 235, 0.25)"
stroke="#2563eb"
/>`AreaClosed` needs the y scale so it can determine the baseline. Include zero in the scale domain unless a truncated baseline is intentional and clearly communicated.
Render a donut with explicit arc keysdraw-donut-chart
import { Pie } from '@visx/shape';
<Pie
data={segments}
pieValue={(d) => d.value}
innerRadius={60}
outerRadius={100}
padAngle={0.01}
>
{({ arcs, path }) => arcs.map((arc) => (
<path key={arc.data.id} d={path(arc) ?? undefined} fill={color(arc.data.id)} />
))}
</Pie>Use a stable data identifier as the React key. Pie angles encode part-to-whole poorly when users need close value comparisons, so prefer bars for that task.
Place labels at arc centroidslabel-pie-segments
<Pie
data={segments}
pieValue={(d) => d.value}
outerRadius={radius}
centroid={([x, y], arc) => (
arc.endAngle - arc.startAngle > 0.25
? <text x={x} y={y} textAnchor="middle" dominantBaseline="middle">{arc.data.label}</text>
: null
)}
/>Centroid labels can overlap or become unreadable on small arcs. Hide them below a threshold and provide the full values in adjacent text or an accessible table.
Generate grouped bar geometrydraw-grouped-bars
import { BarGroup } from '@visx/shape';
<BarGroup
data={data}
keys={keys}
height={innerHeight}
x0={(d) => d.category}
x0Scale={categoryScale}
x1Scale={seriesScale}
yScale={valueScale}
color={(key) => colors[key]}
/>BarGroup expects a band scale for categories, another band scale for series keys within each category, and a value scale. Axes and legends are separate components.
Build a stacked bar chartdraw-stacked-bars
import { BarStack } from '@visx/shape';
<BarStack
data={data}
keys={keys}
x={(d) => d.category}
xScale={xScale}
yScale={yScale}
color={(key) => colorScale(key)}
value={(d, key) => Number(d[key] ?? 0)}
/>Set the y domain from the totals across all stack keys, not from the largest individual value, or the generated bars can exceed the chart bounds.
Use a shape's path generator in custom markupcustomize-generated-path
<LinePath data={points} x={(d) => x(d.x)} y={(d) => y(d.y)}>
{({ path }) => (
<path
d={path(points) ?? undefined}
fill="none"
stroke="url(#line-gradient)"
strokeWidth={3}
/>
)}
</LinePath>A render child replaces the component's default path element. You become responsible for the SVG props, accessibility hooks, and null path result.
Attach pointer and keyboard behavior to a baradd-bar-interaction
<Bar
x={barX}
y={barY}
width={barWidth}
height={barHeight}
fill="#2563eb"
tabIndex={0}
role="img"
aria-label={`${datum.label}: ${datum.value}`}
onPointerMove={(event) => showTooltip(event, datum)}
onFocus={(event) => showTooltip(event, datum)}
onBlur={hideTooltip}
/>Shape components forward SVG props, but tooltips and semantics are yours. Mirror pointer behavior for keyboard focus and provide a nonvisual summary for complex charts.
Keep shape coordinates inside chart marginstranslate-chart-margins
import { Group } from '@visx/group';
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
<svg width={width} height={height}>
<Group left={margin.left} top={margin.top}>
{renderShapes({ width: innerWidth, height: innerHeight })}
</Group>
</svg>Use inner dimensions for every scale range. Mixing outer width with a translated Group is a common cause of clipped marks and misaligned axes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| recharts | npm | Choose it when product teams want composed React charts with axes, legends, tooltips, and responsive containers supplied |
| @nivo/core | npm | Choose Nivo's packages when polished chart components, themes, animation, and Canvas variants matter more than low-level SVG control |
| victory | npm | Choose it for a higher-level declarative React chart API that also has a React Native story |
| d3-shape | npm | Choose it when you only need path generators and do not want React components or the wider visx package family |