# Set transform-box on Animated SVG

**MUST** · **ID:** `animations-svg-transform-box` · **Category:** animations
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-svg-transform-box

> MUST: Transform animated SVG shapes on a `<g>` wrapper with `transform-box: fill-box` and `transform-origin: center` — the CSS default `view-box` resolves the origin against the `viewBox`, so an off-centre shape orbits the canvas instead of spinning in place.

Wrap animated SVG shapes in a <g> and set transform-box: fill-box with transform-origin: center

CSS transforms on SVG elements default to `transform-box: view-box`, so `transform-origin: center` resolves to the centre of the SVG viewport — not the centre of the shape. Rotate or scale an icon that sits anywhere off-centre in its viewBox and it orbits the canvas instead of spinning in place. Setting `transform-box: fill-box` re-points the origin at the element's own bounding box, which is almost always what you meant. Put it on a <g> wrapper so the whole shape shares one origin and the transform does not have to be repeated per path.

## Rule snippet

```tsx
g.spinner { transform-box: fill-box; transform-origin: center; animation: spin 1s linear infinite; }
```

## Bad — do not do this

`animations-svg-transform-box-bad`

```tsx
import { useState } from 'react';

/**
 * Bad: a CSS transform on an SVG group with no `transform-box`. The default is
 * `view-box`, so `transform-origin: center` means the centre of the *viewBox*,
 * not the centre of the shape. The arrow orbits the canvas instead of spinning.
 */
export function SvgTransformBoxBad() {
  const [spun, setSpun] = useState(false);

  return (
    <div className="w-full max-w-sm space-y-4">
      <button
        onClick={() => setSpun((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        Spin the arrow
      </button>

      <div className="rounded-lg bg-muted p-3">
        <svg viewBox="0 0 200 80" className="w-full h-20 text-primary" role="img" aria-label="Arrow rotating around the viewBox origin">
          <circle
            cx="160"
            cy="40"
            r="26"
            fill="none"
            stroke="currentColor"
            strokeDasharray="4 4"
            className="text-muted-foreground"
          />
          <g
            style={{
              // No transform-box: the origin is the viewBox centre (100, 40),
              // 60px away from the shape — so it swings right across the canvas.
              transformOrigin: 'center',
              transform: spun ? 'rotate(180deg)' : 'rotate(0deg)',
              transition: 'transform 600ms cubic-bezier(0.23, 1, 0.32, 1)',
            }}
          >
            <polygon points="144,24 182,40 144,56" fill="currentColor" />
          </g>
        </svg>
      </div>

      <p className="text-xs text-error">
        The arrow flies out of its dashed home — the rotation pivots on the viewBox origin, not the shape
      </p>
    </div>
  );
}
```

## Good — do this

`animations-svg-transform-box-good`

```tsx
import { useState } from 'react';
import { useMediaQuery } from '@/hooks/useMediaQuery';

/**
 * Good: `transform-box: fill-box` re-points `transform-origin` at the group's
 * own bounding box, so `center` means the centre of the arrow. It spins in
 * place, wherever it happens to sit in the viewBox.
 */
export function SvgTransformBoxGood() {
  const [spun, setSpun] = useState(false);
  const reduced = useMediaQuery('(prefers-reduced-motion: reduce)');

  return (
    <div className="w-full max-w-sm space-y-4">
      <button
        onClick={() => setSpun((v) => !v)}
        className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        Spin the arrow
      </button>

      <div className="rounded-lg bg-muted p-3">
        <svg viewBox="0 0 200 80" className="w-full h-20 text-primary" role="img" aria-label="Arrow rotating around its own centre">
          <circle
            cx="160"
            cy="40"
            r="26"
            fill="none"
            stroke="currentColor"
            strokeDasharray="4 4"
            className="text-muted-foreground"
          />
          <g
            style={{
              transformBox: 'fill-box',
              transformOrigin: 'center',
              transform: spun ? 'rotate(180deg)' : 'rotate(0deg)',
              transition: reduced
                ? 'none'
                : 'transform 600ms cubic-bezier(0.23, 1, 0.32, 1)',
            }}
          >
            <polygon points="144,24 182,40 144,56" fill="currentColor" />
          </g>
        </svg>
      </div>

      <p className="text-xs text-success">
        transform-box: fill-box + transform-origin: center — the arrow spins in place. Reduced motion flips it instantly.
      </p>
    </div>
  );
}
```

## References

- [Vercel Web Interface Guidelines](https://github.com/vercel-labs/web-interface-guidelines)
- [MDN — transform-box](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/transform-box)
- [MDN — <g>](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/g)
