# SVG

_Box Kite 2.2.6 · a markdown copy of https://www.box-kite.dev/svg/_

Twenty-three SVG properties as typed props, and twenty components to put them on — paint, stroke, text and the SVG 2 geometry that lets a shape move with no JavaScript at all.

**Paint**

```jsx
<Svg viewBox="0 0 120 48" width="120px" fill="violet-500" stroke="violet-300" strokeWidth={2}>
  <Circle cx={24} cy={24} r={16} />
  <Rect x={52} y={8} width={32} height={32} fillOpacity={0.4} />
  <Circle cx={104} cy={24} r={16} fill="none" strokeWidth={4} strokeOpacity={0.5} />
</Svg>
```

## Set them once, on the element above

Every paint and stroke property here except `vectorEffect` is an inherited one, so a value on the `<svg>` reaches every shape inside it — that is why the demo above sets `stroke` once rather than on all three shapes. A shape that wants something else states it, and wins for itself.

The one thing inheritance cannot beat is a `fill` or `stroke` _attribute_ written on the shape itself: a presentation attribute on an element outranks a value inherited from its parent. Icon sets that hard-code `fill="currentColor"` on each path are the common case — style those paths, not the wrapper.

## The numbers are user units

`strokeWidth`, `strokeDasharray`, `strokeDashoffset` and `strokeMiterlimit` take the number and pass it straight through — `strokeWidth={2}` is `stroke-width: 2`. No divider is applied, because an SVG length is measured in the coordinate system the `viewBox` sets up, not in rem or in pixels. That is the same number you would have written in the attribute.

`fillOpacity` and `strokeOpacity` use the same 0–1 scale as `opacity`, in tenths.

**Dashes**

```jsx
<Svg viewBox="0 0 200 12" width="200px" stroke="emerald-500" strokeWidth={4} fill="none" strokeLinecap="round">
  <Line x1={4} y1={6} x2={196} y2={6} strokeDasharray={12} />
</Svg>
```

## A pattern is a number or a string

`strokeDasharray={12}` is a 12-long dash and a 12-long gap. Anything else is the CSS value as a string — `"12 4"` for a long dash and a short gap, `"1 8"` with a round cap for a dotted line. The space survives the class name: it becomes an underscore there, so `strokeDasharray="12 4"` is the class `strokeDasharray-12_4` and one rule.

**Drawing a path on hover**

```jsx
// The track is drawn once. The line over it starts pushed off its own dash and slides back on hover.
<Box position="relative" width={50} height={12}>
  <Svg viewBox="0 0 200 48" width="200px" fill="none" stroke="slate-200" strokeWidth={3} strokeLinecap="round">
    <Path d="M8 40 L56 12 L104 34 L152 8 L192 24" />
  </Svg>
  <Svg
    viewBox="0 0 200 48"
    width="200px"
    position="absolute"
    inset={0}
    fill="none"
    stroke="violet-500"
    strokeWidth={3}
    strokeLinecap="round"
    strokeLinejoin="round"
    strokeDasharray={320}
    strokeDashoffset={320}
    hover={{ strokeDashoffset: 0 }}
  >
    <Path d="M8 40 L56 12 L104 34 L152 8 L192 24" />
  </Svg>
</Box>
```

## The transition is already there

Nothing in that example declares a transition. Every Box transitions on `--transitionTime`, and an `<svg>` and the shapes inside it transition on `--svgTransitionTime` — so moving `strokeDashoffset` under `hover` is the whole animation.

Which also means it stops when it should: a reader with `prefers-reduced-motion: reduce` gets both variables set to `0s`, and the line simply appears.

**Caps and joins**

```jsx
<Svg viewBox="0 0 72 40" width="72px" fill="none" stroke="amber-500" strokeWidth={12} strokeLinejoin="round" strokeLinecap="round">
  <Path d="M10 32 L36 10 L62 32" />
</Svg>
```

**Fill rule**

```jsx
<Svg viewBox="0 0 64 64" width="64px" fill="sky-500" fillRule="evenodd">
  <Path d="M32 4 L39 24 L60 24 L43 37 L50 58 L32 45 L14 58 L21 37 L4 24 L25 24 Z" />
</Svg>
```

**A stroke that ignores the scale**

```jsx
<Svg viewBox="0 0 12 12" width="96px" fill="none" stroke="rose-500" strokeWidth={1} vectorEffect="non-scaling-stroke">
  <Path d="M1 11 L6 1 L11 11 Z" />
</Svg>
```

## vectorEffect is the exception

Both shapes above are drawn at `strokeWidth={1}` in a 12-unit `viewBox` blown up to 96 pixels, so the left one comes out eight units thick. `non-scaling-stroke` measures the stroke after the transform instead, which is what a chart redrawn at any width needs.

Alone among the paint and stroke properties, `vector-effect` is not inherited — a value on the `<svg>` would reach nothing. So this prop, and only this prop, writes a rule that names the element and its descendants: `.vectorEffect-non-scaling-stroke, .vectorEffect-non-scaling-stroke *`. Put it wherever it reads best.

**Outlined text**

```jsx
<Svg viewBox="0 0 200 48" width="200px" fill="white" stroke="indigo-600" strokeWidth={6} paintOrder="stroke" strokeLinejoin="round">
  <SvgText x={8} y={36} fontSize={36} fontWeight={700}>
    Box
  </SvgText>
</Svg>
```

## A shape's own numbers are props too

SVG 2 made the geometry attributes real CSS properties, so `cx`, `cy`, `r`, `rx`, `ry`, `x` and `y` are Box props here. They are user units like every other SVG length on this page — `r={20}` is `r: 20` — and a percentage is of the viewport the `viewBox` describes.

Which one applies to which element is the SVG spec's answer, not this library's. `cx` and `cy` centre a `<circle>` or an `<ellipse>`; `r` is the circle's radius; `rx` and `ry` are the ellipse's two radii and a `<rect>`'s corners; `x` and `y` position a `<rect>`, `<image>`, `<use>`, `<foreignObject>` or a nested `<svg>`. On an element with no such geometry the property is simply ignored, exactly as the attribute would be.

They are not inherited, and here that is the right behaviour rather than the problem it was for `vectorEffect` — a radius handed down to every shape below would be nonsense. Set them on the shape.

Two names are missing from the list, and they are the reason the elements have components of their own. A `<rect>`'s `width` and `height` are CSS properties too, but those prop names were taken years ago by the layout scale, where `width={32}` means `8rem`. So `<Rect>` claims them back for itself: `<Rect width={40} height={40} />` is forty user units square, written as an attribute the way SVG writes it.

**Geometry moves**

```jsx
// Geometry is CSS, so it transitions. There is no JavaScript in this.
<Svg viewBox="0 0 160 56" width="160px" fill="violet-500">
  <Circle cx={28} cy={28} r={12} hover={{ r: 22 }} />
  <Circle cx={80} cy={28} r={12} hover={{ cy: 14, r: 8 }} />
  <Ellipse cx={132} cy={28} rx={20} ry={10} hover={{ rx: 10, ry: 20 }} />
</Svg>
```

**Corners and position**

```jsx
<Svg viewBox="0 0 56 56" width="56px" fill="teal-500">
  <Rect width={40} height={40} x={8} y={8} rx={4} hover={{ rx: 20 }} />
</Svg>
```

## rx is not borderRadius

The two read alike and mean different numbers. `borderRadius={8}` is on the spacing scale and comes out as `2rem`; `rx={8}` is eight user units inside the `viewBox`. A rect wants `rx`.

**Anchoring a label**

```jsx
<Svg viewBox="0 0 200 40" width="200px" fill="slate-700" textAnchor="middle">
  <SvgText x={100} y={22} fontSize={14}>
    the label
  </SvgText>
  <Line x1={100} y1={26} x2={100} y2={38} stroke="rose-400" strokeWidth={2} />
</Svg>
```

## dominantBaseline is the vertical half

`textAnchor` decides which part of the text sits on its `x`; `dominantBaseline` decides which part sits on its `y`. `central` centres a number inside a gauge, `hanging` drops a label below an axis line, and `alphabetic` is where every browser starts.

This is the second property CSS does not inherit — `vectorEffect` was the first — so it gets the same treatment: a rule that names the element and its descendants. Set it on the `<svg>` and every label inside obeys.

Text size is the ordinary `fontSize` prop, divider 16 like everywhere else. Inside an `<svg>` a pixel _is_ a user unit, so `fontSize={20}` is 20 units and scales with the `viewBox` along with the shapes.

**Baselines against a line**

```jsx
<Svg viewBox="0 0 120 40" width="120px" fill="slate-700" dominantBaseline="central">
  <Line x1={0} y1={20} x2={120} y2={20} stroke="rose-400" strokeWidth={1} />
  <SvgText x={8} y={20} fontSize={14}>
    the label
  </SvgText>
</Svg>
```

**A gauge, with no JavaScript**

```jsx
// The ring is one dash as long as the circle's own circumference, pushed off the path and then
// pulled back. The number is placed at the centre point and centred on it by the two text props.
<Svg viewBox="0 0 96 96" width="96px" className="gauge" fill="none" strokeLinecap="round">
  <Circle cx={48} cy={48} r={38} stroke="slate-200" strokeWidth={10} />
  <Circle
    transform="rotate(-90 48 48)"
    cx={48}
    cy={48}
    r={38}
    stroke="indigo-600"
    strokeWidth={10}
    strokeDasharray={239}
    strokeDashoffset={239}
    hoverGroup={{ gauge: { strokeDashoffset: 60, r: 40 } }}
  />
  <SvgText x={48} y={48} textAnchor="middle" dominantBaseline="central" fontSize={20} fill="slate-700">
    75%
  </SvgText>
</Svg>
```

## What the gauge is made of

Four props and no state. `strokeDasharray={239}` makes the ring one dash as long as its own circumference — 2π × 38 is about 239 — and `strokeDashoffset={239}` pushes that dash entirely off the path, so nothing shows. Moving the offset to 60 leaves 179 of the 239 drawn, which is the three quarters the label claims.

`r` grows by two units at the same time, which is the geometry tier doing something the attribute cannot. The number in the middle sits at the centre point and is centred on it by `textAnchor` and `dominantBaseline` — that pair is the whole reason SVG text is awkward to place by hand.

One thing here is still an attribute, and `<Circle>` takes it as a prop of its own: `transform="rotate(-90 48 48)"` starts the arc at twelve o'clock. It is the SVG attribute rather than the `rotate` prop because it carries its own centre of rotation — CSS would turn the circle around the corner of the `viewBox` instead.

The transition is the one already on every shape inside an `<svg>`, so a reader with `prefers-reduced-motion: reduce` gets a gauge that is simply there, with no sweep.

## Every element is a component

`@box-kite/react/components/svg` is twenty components, one per element: `Svg`, `G`, `Defs`, `Path`, `Circle`, `Ellipse`, `Rect`, `Line`, `Polyline`, `Polygon`, `SvgText`, `TSpan`, `LinearGradient`, `RadialGradient`, `Stop`, `ClipPath`, `Mask`, `Use`, `SvgSymbol` and `Marker`. Each one is a Box, so every prop above works on it — and every demo on this page is built from them, without a single `tag`.

Two are not named after their element. `SvgText` is `<text>`, because one library cannot have a `Text` that means an SVG element and a `Text` that means a paragraph. `SvgSymbol` is `<symbol>`, because `Symbol` is a global that no module should quietly shadow.

## Where a name means two things

An SVG attribute and a Box prop can be the same word, and the clash is silent: a prop table that takes `d` for `display` writes a path no browser can draw, and reports nothing. Here `d` is already the shorthand for `flexDirection`, a `<rect>`'s `width` is the ÷4 layout scale, and a `<text>`'s `x` is a CSS geometry property that does not apply to text at all. So each component settles those names for its own element: on `Path`, `d` is path data; on `Rect`, `width` is user units; on `SvgText`, `x` is the attribute. Everywhere else they keep their Box meaning.

One name can be answered twice, because the answer belongs to the element and not to the word. `cx` on a `Circle` is the CSS property, and transitions. `cx` on a `RadialGradient` is an attribute, because CSS geometry does not reach a gradient. Both are typed, and neither needs `props`.

`d` is still not a styling prop even though CSS defines one: Safari does not support it, and a path that silently refuses to draw is worse than an attribute that always does. A paint server, on the other hand, no longer needs `props` at all: `fill` and `stroke` take `url(#sky)` and `var(--chart-1)` beside the palette, and so does `clipPath` — see the illustration below.

| Component | Element | Attributes it takes as props |
| --- | --- | --- |
| `Svg` | <svg> | viewBox, preserveAspectRatio, width, height, label |
| `G` | <g> | transform |
| `Defs` | <defs> | — |
| `Path` | <path> | d, transform, pathLength |
| `Circle` | <circle> | transform, pathLength |
| `Ellipse` | <ellipse> | transform, pathLength |
| `Rect` | <rect> | width, height, transform, pathLength |
| `Line` | <line> | x1, y1, x2, y2, transform, pathLength |
| `Polyline` | <polyline> | points, transform, pathLength |
| `Polygon` | <polygon> | points, transform, pathLength |
| `SvgText` | <text> | x, y, dx, dy, textLength, lengthAdjust, transform |
| `TSpan` | <tspan> | x, y, dx, dy, textLength, lengthAdjust |
| `LinearGradient` | <linearGradient> | x1, y1, x2, y2, gradientUnits, gradientTransform, spreadMethod |
| `RadialGradient` | <radialGradient> | cx, cy, r, fx, fy, gradientUnits, gradientTransform, spreadMethod |
| `Stop` | <stop> | offset, stopColor, stopOpacity |
| `ClipPath` | <clipPath> | clipPathUnits, transform |
| `Mask` | <mask> | maskUnits, maskContentUnits, x, y, width, height |
| `Use` | <use> | href, width, height, transform |
| `SvgSymbol` | <symbol> | viewBox, preserveAspectRatio, x, y, width, height |
| `Marker` | <marker> | markerWidth, markerHeight, refX, refY, orient, markerUnits, viewBox |

## A drawing says whether it means anything

An `<Svg>` with no `label` is `aria-hidden`. Most SVG on a page is decoration sitting beside the words that already say it, and a screen reader should walk past it — which is what nothing at all fails to say. Give it a `label` and it becomes `role="img"` with that name instead. State a role or an `aria-labelledby` of your own in `props` and the component steps out of the way entirely.

**An illustration**

```jsx
<Svg viewBox="0 0 200 120" width="100%" className="scene" label="A sun rising between two hills">
  <Defs>
    <LinearGradient id="sky" x1={0} y1={0} x2={0} y2={1}>
      <Stop offset="0%" stopColor="currentColor" color="indigo-500" />
      <Stop offset="100%" stopColor="currentColor" color="amber-200" />
    </LinearGradient>
    <ClipPath id="frame">
      <Rect width={200} height={120} rx={10} />
    </ClipPath>
  </Defs>
  <G clipPath="url(#frame)">
    <Rect width={200} height={120} fill="url(#sky)" />
    <Circle cx={64} cy={78} r={18} fill="amber-300" hoverGroup={{ scene: { cy: 44, r: 22 } }} />
    <Path d="M-10 120 L64 68 L138 120 Z" fill="emerald-800" />
    <Path d="M92 120 L156 56 L220 120 Z" fill="emerald-700" />
  </G>
</Svg>
```

## What the illustration used to need props for

Two of these three were attributes until the chart primitives needed them to be values. The sky is `fill="url(#sky)"` and the rounded frame is `clipPath="url(#frame)"`: a reference to something the document defines is a paint value like any other now, so it can differ per theme, on `hover` and per breakpoint, which an attribute never could. The third is not a gap at all — a gradient stop paints itself with `stopColor="currentColor"` so that the Box `color` prop, themed like everything else, is what actually decides it.

**A chart, by hand**

```jsx
// Six bars, an axis, a label under each, and a trend line that draws itself on hover.
<Svg viewBox="0 0 240 124" width="100%" className="chart" label="Revenue by month">
  <Line x1={8} y1={96} x2={232} y2={96} stroke="slate-300" strokeWidth={1} />
  {revenue.map(({ month, value }, index) => (
    <Rect key={month} x={16 + index * 36} y={96 - value} width={24} height={value} rx={3} fill="sky-600" hover={{ fill: 'sky-400' }} />
  ))}
  <Polyline
    points={revenue.map(({ value }, index) => `${28 + index * 36},${96 - value}`).join(' ')}
    fill="none"
    stroke="amber-400"
    strokeWidth={2}
    strokeLinecap="round"
    strokeDasharray={260}
    strokeDashoffset={260}
    hoverGroup={{ chart: { strokeDashoffset: 0 } }}
  />
  {revenue.map(({ month }, index) => (
    <SvgText key={month} x={28 + index * 36} y={112} textAnchor="middle" fontSize={11} fill="slate-500">
      {month}
    </SvgText>
  ))}
</Svg>
```

## What the chart is, and is not

A bar is a `<Rect>` whose `y` is a CSS property and whose `height` is an attribute — the split this page has been describing, in one element. The axis is a `<Line>`, the months are `<SvgText>` centred with `textAnchor`, and the trend is a `<Polyline>` drawing itself with the dash trick from further up the page. Nothing here is a chart library, and there is no state.

What it is not is a chart _component_ — no scales, no ticks, no tooltip, no responsiveness beyond what the `viewBox` gives for free. Those are the next step. What this page shows is that the primitives underneath them already exist.

## Every prop

| Prop | CSS property | Values |
| --- | --- | --- |
| `fill` | fill | any colour variable, or none |
| `fillOpacity` | fill-opacity | 0 – 1 in tenths |
| `fillRule` | fill-rule | nonzero, evenodd |
| `stroke` | stroke | any colour variable, or none |
| `strokeOpacity` | stroke-opacity | 0 – 1 in tenths |
| `strokeWidth` | stroke-width | a number, in user units |
| `strokeLinecap` | stroke-linecap | butt, round, square |
| `strokeLinejoin` | stroke-linejoin | miter, round, bevel |
| `strokeMiterlimit` | stroke-miterlimit | a number, 1 or greater |
| `strokeDasharray` | stroke-dasharray | a number, or the pattern as a string |
| `strokeDashoffset` | stroke-dashoffset | a number, or a percentage of the path length |
| `paintOrder` | paint-order | normal, fill, stroke, markers |
| `vectorEffect` | vector-effect | none, non-scaling-stroke |
| `shapeRendering` | shape-rendering | auto, optimizeSpeed, crispEdges, geometricPrecision |
| `textAnchor` | text-anchor | start, middle, end |
| `dominantBaseline` | dominant-baseline | auto, alphabetic, central, middle, hanging, text-top, text-bottom, ideographic, mathematical |
| `cx` | cx | a number in user units, or a percentage |
| `cy` | cy | a number in user units, or a percentage |
| `r` | r | a number in user units, or a percentage |
| `rx` | rx | a number, a percentage, or auto |
| `ry` | ry | a number, a percentage, or auto |
| `x` | x | a number in user units, or a percentage |
| `y` | y | a number in user units, or a percentage |

## Themes, pseudo-classes, breakpoints

These are ordinary Box props, so everything that nests around a Box prop nests around them. A stroke that changes with the theme, a fill that reacts to hover and a width that grows at a breakpoint are all one prop each.

```jsx
<Svg
  viewBox="0 0 24 24"
  fill="none"
  strokeWidth={2}
  theme={{ dark: { stroke: 'slate-300' }, light: { stroke: 'slate-700' } }}
  hover={{ strokeWidth: 3 }}
  md={{ strokeWidth: 1.5 }}
>
  <Path d="M4 12h16" />
</Svg>
```

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

