Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 13.1Intermediate ~22 min · shot 1 of 6

SVG for Motion

In one line

An SVG is a drawing with its own coordinates. CSS can animate its transforms, colours and strokes, but a shape turns around the top-left corner of the drawing unless you set transform-box: fill-box or a pivot point.

0/6 · 0%

Think of it like this

A paper cut-out pinned to a board. Pin it through the middle and it spins in place. Pin it in the top-left corner of the board and it swings around that corner, maybe right off the board. SVG pins every shape in the top-left corner of the whole drawing unless you say otherwise.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

viewBox
The window onto an SVG drawing: min-x min-y width height in user units. The browser scales it to the size you give the <svg>.
user unit
One step in the drawing's own coordinates. In SVG transforms, a CSS px means a user unit.
transform-origin
The point a transform turns or scales around. In SVG it starts at the top-left corner of the drawing.
transform-box
Which box the origin refers to: view-box (the whole drawing, the SVG default), fill-box (the shape's own box) or stroke-box (the box with its stroke).
presentation attribute
A style written as an attribute, like transform="rotate(45)" or fill="red". A CSS rule for the same property wins over it.
<use>
An element that draws a copy of another shape. The copy lives in a hidden tree, which outside CSS selectors cannot reach.

Step by step

01Run the three squares and read the console

"Three squares, one quarter turn" prints the origin and box of each. The blue one says origin 0px 0px, box view-box; the orange one origin 20px 20px, box fill-box; the purple one origin 260px 60px, box view-box. Three animations, one per square.

02Slow it down and watch the blue one leave

Press Replay, then Pause and speed 0.1×. At the middle of the turn all three tracks show 45 degrees: the pictures differ only because the pivot differs. At the end the blue square is gone, because a quarter turn around the top-left corner moved its centre to x = -60.

03Read the use example

"Three copies of one gem" counts three <use> elements and two animations: pulse on rect#gem and spin on use#u2. Three gems pulse at once, yet only one pulse track exists.

04Find the gem that spins in place

Step through the middle gem's turn: it spins on its own centre, because translate: 130px 40px places it and the origin 15px 15px is its centre.

Try it yourself

  1. 1

    Take away fill-box

    In the first example change #b { transform-box: fill-box; transform-origin: center; } to #b { transform-origin: center; } and press Replay. The orange square no longer turns in place: it circles the centre of the drawing and ends 10 units lower and 10 to the left, centred on (150, 70). The console now prints box view-box for it.

  2. 2

    Move the pivot by hand

    Change the purple square's transform-origin: 260px 60px to 240px 40px (its own top-left corner). It now turns like a door on a hinge: at 600 ms that corner has not moved.

  3. 3

    Count the tracks of the gems

    In the second example pause and click the spin track: CSSAnimation, 2000 ms, linear, infinite, rotate on use#u2. There is no track for the gem on the left or the right, although they pulse too.

Code & diagrams

Three squares, one quarter turn

Same keyframes on three squares. Pause at 0.1× and step: the blue square swings around the drawing's top-left corner and leaves the picture.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

a: origin 0px 0px, box view-box
b: origin 20px 20px, box fill-box
c: origin 260px 60px, box view-box
animations: 3
Three copies of one gem

The pulse is defined once on the source and runs separately in each copy, offset by `--d`. The timeline lists the source only.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

copies drawn: 3
animations the page can see: 2
  pulse on rect#gem
  spin on use#u2
gem's own delay: 0s

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Style a copy from outside

In the second example add #u1 rect { fill: red; } to the CSS.

terminal
$ Look at the left gem
── what you'll see ──
Nothing changes: it still pulses blue to orange. The selector matches no element, because the rect inside a `<use>` is not in the page's tree.

Break #2

Position a spinning copy with x and y

In the second example change the middle copy to <use id="u2" href="#gem" x="130" y="40" class="spin" /> and the keyframes to @keyframes spin { to { transform: rotate(360deg); } }.

terminal
$ Pause the timeline and step to where the `spin` track is at 45 degrees (250 ms into its 2000 ms turn), then to 1000 ms
── what you'll see ──
The gem is not at the middle any more: it swings in a big circle around the top-left of the drawing. At 250 ms only a sliver of it peeks in at the bottom edge, and at 1000 ms (half a turn) it is out of the picture, far to the left. The computed `transform-origin` is `15px 15px`.

Myth vs fact

Myth

transform-origin: center turns an SVG shape around its centre, as in HTML.

Fact

Only with transform-box: fill-box. Without it, center means the centre of the whole drawing (measured: the square circled it).

Myth

A CSS transform adds to the transform attribute.

Fact

It replaces it (measured).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    fill-box is the shape's geometry without its stroke. Use stroke-box when a thick stroke matters for where the centre or edge should be.

  • ▸

    A transform or rotate on an SVG shape measured as composite, like on an HTML box. Read the track colour for your own drawing, then check DevTools' Paint flashing (Topic 6.5).

  • ▸

    <use> is how icon sprites work: one hidden <svg> with <symbol> elements and a <use href="#icon"> per place. Animation on a sprite icon must be reachable through the <use> itself or custom properties, never through the symbol's inner selectors.

Remember this

  1. 1

    The problem this solves: you draw a shape in SVG, add transform: rotate(90deg) and the shape flies away instead of turning on the spot. Nothing is wrong with the browser. The turning point (the transform-origin) means something different in SVG than in HTML, and this topic shows what, with measured numbers.

  2. 2

    An SVG has its own coordinates. viewBox="0 0 300 120" is a window onto a drawing 300 units wide and 120 high. A user unit is one step in those coordinates. The width and height on the <svg> set the size on screen. Shown 600 px wide, one unit is 2 px. In an SVG, a CSS px inside a transform means one user unit: we measured that translate(10px, 0) on a shape in a drawing shown at 2× moved it 20 screen pixels. The whole drawing scales together.

  3. 3

    What CSS can animate in an SVG. The same engine as for HTML: transform, translate, rotate, scale, opacity, fill, stroke, stroke-width, stroke-dashoffset (Topic 13.2), offset-distance (13.3) and d (13.4). Chromium also lets CSS set cx, cy, r, x, y, width and height (CSS.supports says yes). The cost table (measured in headless Chromium) says transform and opacity are composite, fill, stroke, stroke-dashoffset and offset-distance are paint (amber), and cx, cy, r, d and stroke-width are layout (red: the geometry is recomputed every frame). Prefer transform to move a shape, and keep geometry morphs short.

  4. 4

    The transform-origin surprise. In HTML the turning point is the centre of the element's box. In SVG the default is the top-left corner of the drawing: we measured 0px 0px. transform-box says which box the origin refers to. view-box is the default for SVG (the whole drawing). fill-box is the shape's own box, without its stroke. stroke-box adds the stroke. With transform-box: fill-box; transform-origin: center the origin of a 40-unit square measured 20px 20px, its own centre. The other way is a point in the drawing's units, such as transform-origin: 260px 60px, which is right for a deliberate pivot like a wheel hub.

  5. 5

    **Forgetting fill-box still goes wrong, only less.** transform-origin: center alone, with the default view-box, means the centre of the whole drawing: the orange square then circled that point (tryIt 1). The blue square left the edge: a quarter turn around (0, 0) puts its centre at (-60, 60).

  6. 6

    Attribute or CSS, never both. transform="rotate(45)" in the markup is a presentation attribute. A CSS transform on the same element replaces it, it does not add to it: an attribute translate(50 0) plus a CSS rotate(10deg) measured a matrix with no translation left. The syntax differs too: the attribute takes unitless degrees and spaces, CSS needs units, and an invalid CSS value is ignored without a message. To keep a placement, put it on a wrapper <g transform="…"> and animate the shape inside.

  7. 7

    **<use> copies.** <use href="#gem"> draws a shape defined once somewhere else, as many times as you like. The copies live in a hidden tree. We measured four facts. A rule on the source applies to every copy and each copy animates on its own: animation-delay: var(--d) with a different --d on each <use> made three copies pulse out of step, because custom properties pass into the copy. document.getAnimations() listed only one pulse (on the source), so the timeline shows one track for three moving things. A selector outside cannot reach a copy (breakIt 1). Animating the <use> itself works, but its x and y are applied inside it, so fill-box measured 15px 15px (breakIt 2). Place the copy with the translate property instead, as the second example does.

Explain it without notes

01

Why does a rotated SVG shape sometimes leave the screen, and how do you fix it?

02

How do you give each <use> copy of one shape its own animation timing?

Practice

01

Make a star shape (#star) pulse (scale 1 to 1.3 and back) around its own centre, forever, and give it a reduced-motion rule.

02

Three <use> copies of #dot should blink one after another, 150 ms apart. Write the CSS and the markup.

Trade-offs

  • ↔

    <use> keeps the markup small, but the copies cannot be styled from outside and the timeline cannot list them. Draw the shape out when each copy needs its own selectors or tracks.

Done when you can

  • You can say where an SVG shape turns by default and fix it two ways

  • You can say what fill-box and view-box mean

  • You can predict that a CSS transform replaces the attribute

  • You can animate <use> copies out of step and know what the timeline will show