<!-- Source: https://motion.svelte.page/docs/svg-animation -->

# SVG Animation

> Bind MotionValues to SVG attributes, draw paths, and commit exact final styles after accelerated animation.

**Source:** [https://motion.svelte.page/docs/svg-animation](https://motion.svelte.page/docs/svg-animation)

---

Any `MotionValue` can drive an SVG presentation attribute directly. Pass it as a
prop and Motion subscribes to it — no `animate` prop, no keyframes, and no
component re-render when the value changes.

```svelte
<script lang="ts">
  import { motion, useSpring } from '@humanspeak/svelte-motion'

  const cx = useSpring(40)
</script>

<svg viewBox="0 0 300 120">
  <motion.circle {cx} cy={60} r={12} fill="#60a5fa" />
</svg>
```

> Live example: [/examples/svg-animation](https://motion.svelte.page/examples/svg-animation)

## Motion 13: accelerated animations commit their final styles

Motion 13 tightened SVG animation completion: when a browser-accelerated animation
finishes, Motion commits its exact final `opacity`, `transform`, and paint values to
the SVG element. This matters when an animation fades an element out, when a later
interaction reverses it, or when application state is read immediately after
completion. Without the commit, the picture can look correct for one frame while
computed style still contains the previous value.

```svelte
<script lang="ts">
  import { motion } from '@humanspeak/svelte-motion'

  let docked = $state(false)
</script>

<motion.circle
  cx={24}
  cy={24}
  r={10}
  initial={false}
  animate={{
    opacity: docked ? 0 : 1,
    transform: docked ? 'translateX(120px)' : 'translateX(0px)',
    fill: docked ? '#a855f7' : '#247768'
  }}
/>
```

The important detail is `initial={false}` plus state-driven final values. Once the
transition ends, `getComputedStyle(circle)` reports the destination values exactly;
reversing the state commits the source values again.

> Live example: [/examples/svg-animation#fig-002](https://motion.svelte.page/examples/svg-animation#fig-002)

The signal-dock demo makes the behavior visible: the ghost ring marks the destination,
the packet blooms and fades there, and its telemetry reads the actual browser-computed
opacity, transform, and fill. Use **Replay** and **Reverse** to exercise both commits.

## One DOM channel

Every bound value renders as an SVG **presentation attribute** via
`setAttribute` — `cx`, `stroke-dashoffset`, `x1`/`y1`/`x2`/`y2`, `points`,
all of them. Transforms are the one exception: they compose on the `style`
attribute. This is exactly how React Framer Motion renders SVG.

That means the DOM you inspect is the live value — a bound `cx` moves the
element's `cx` **attribute** every frame, and computed style follows it
(presentation attributes reflect into computed style):

```ts
// After cx.set(60):
circle.getAttribute('cx')      // "60"   <- the live value
getComputedStyle(circle).cx    // "60px" <- follows the attribute
```

One cascade note: presentation attributes sit at the very bottom of the CSS
cascade, so a stylesheet rule targeting the same geometry property (e.g.
`circle { cx: … }`) wins over the animated attribute — the same trade-off
React Framer Motion makes.

## Prop names use the DOM spelling

Svelte templates take the DOM spelling of hyphenated attributes, so write
`stroke-width`, not `strokeWidth`:

```svelte
<script lang="ts">
  import { motion, useSpring } from '@humanspeak/svelte-motion'

  const strokeWidth = useSpring(4)
</script>

<svg viewBox="0 0 120 120">
  <motion.circle
    cx={60}
    cy={60}
    r={20}
    fill="none"
    stroke="#5eead4"
    stroke-width={strokeWidth}
  />
</svg>
```

Both spellings are accepted, but the kebab-case form is the one you would write
for a plain `<circle>`, and it is what the rendered markup uses.

## Server rendering

A bound attribute is server-rendered with the MotionValue's current value, so
the first paint is correct and nothing flashes on hydration. Attribute names are
emitted with their DOM spelling — `strokeDashoffset` becomes
`stroke-dashoffset`, while genuinely camelCase names like `viewBox` are left
alone.

## attrX, attrY, and attrScale

`x`, `y`, and `scale` are ambiguous: each is both an SVG attribute and a CSS
transform that Motion already owns. Passing `x` moves the element with a
transform. To reach the **attribute** instead, use `attrX`, `attrY`, or
`attrScale`:

```svelte
<script lang="ts">
  import { motion, useMotionValue } from '@humanspeak/svelte-motion'

  const attrX = useMotionValue(10)
</script>

<svg viewBox="0 0 300 120">
  <!-- renders x="10", not transform: translateX(10px) -->
  <motion.rect {attrX} attrY={10} width={40} height={40} />
</svg>
```

<div class="not-prose my-6 rounded-lg border border-amber-500/30 bg-amber-500/5 p-4 text-sm">

**`scale` is not a presentation attribute on shape elements.** `attrScale` on a
`<rect>` writes the attribute faithfully, and the rect ignores it — nothing
moves. It is a real attribute on `<feDisplacementMap>`, where it drives the
displacement amount. This mirrors Framer Motion's behavior.

</div>

<div class="not-prose my-6 rounded-lg border border-amber-500/30 bg-amber-500/5 p-4 text-sm">

**`attrX`/`attrY`/`attrScale` are inert on `<motion.svg>` itself.** The
renderer treats the root `<svg>` tag as HTML-like and returns before the
attribute copy, so a bound `attrX` on a nested `<motion.svg>` leaves the `x`
attribute at its server-rendered value. Position nested `<svg>` elements with
`x`/`y` instead — they animate as transforms. This also mirrors React Framer
Motion; [#456](https://github.com/humanspeak/svelte-motion/issues/456) tracks
lifting the limitation after 1.x.

</div>

## Drawing paths

`pathLength`, `pathSpacing`, and `pathOffset` are handled separately, as
normalized 0–1 values. They animate the underlying `stroke-dasharray` and
`stroke-dashoffset` for you:

```svelte
<motion.path
  d="M 10 100 Q 100 10 190 100"
  stroke="#5eead4"
  fill="none"
  initial={{ pathLength: 0 }}
  animate={{ pathLength: 1 }}
  transition={{ duration: 1.5 }}
/>
```

Set `pathLength` to `0.5` and half the path is drawn, regardless of its actual
length in user units.

## Filter primitives

SVG tag names are case-sensitive, and motion components are addressed in
lowercase. `motion.fedisplacementmap` renders a correctly-cased
`<feDisplacementMap>`:

```svelte
<script lang="ts">
  import { motion, useSpring } from '@humanspeak/svelte-motion'

  const warp = useSpring(12)
</script>

<svg viewBox="0 0 300 120">
  <filter id="warp">
    <feTurbulence type="turbulence" baseFrequency="0.04" numOctaves="2" result="noise" />
    <motion.fedisplacementmap in="SourceGraphic" in2="noise" attrScale={warp} />
  </filter>
  <rect x="40" y="25" width="220" height="70" fill="#38bdf8" filter="url(#warp)" />
</svg>
```

Filter-primitive attributes are bindable like any other. `stdDeviation`,
`baseFrequency`, `numOctaves`, `dx`, `dy`, and `radius` all take a MotionValue:

```svelte
<script lang="ts">
  import { motion, useSpring } from '@humanspeak/svelte-motion'

  const blur = useSpring(2)
</script>

<svg viewBox="0 0 300 120">
  <filter id="soften">
    <motion.fegaussianblur in="SourceGraphic" stdDeviation={blur} />
  </filter>
  <rect x="40" y="25" width="220" height="70" fill="#4ade80" filter="url(#soften)" />
</svg>
```

The same holds for `motion.lineargradient`, `motion.clippath`,
`motion.textpath`, and the rest of the camelCase SVG elements.
