Lesson 2
Transforms & Grouping
Lesson 1 established how the coordinate system works - the infinite stage, the viewBox camera, the viewport screen. This lesson covers how to manipulate that coordinate system: moving, rotating, scaling, and skewing elements within it. This is the transform attribute.
The Core Insight: Transforms Move Space, Not Shapes
When you apply transform="translate(50, 30)" to an element, you're not shifting the element 50 units right and 30 units down. You're creating a new local coordinate system that is shifted 50 right and 30 down - and then drawing the element at its original coordinates within that new system.
The result looks the same, but the mental model matters: transforms modify space, not shapes. This becomes critical when you chain multiple transforms.
Visualising "move the space, not the shape"
Here's the same translate(100, 60) shown two ways. On the left, we show the wrong mental model (shape slides along a fixed grid). On the right, the correct model (grid moves, shape stays at its local coordinates):
Left: the intuitive-but-wrong model - the shape moves along a fixed grid. Right: what actually happens - a new grid (blue axes) is created at an offset, and the shape is drawn at the same local coordinates on the new grid. The visual result is identical, but the "grid moves" model correctly predicts what happens when you add more transforms.
If you then add rotate(30) after the translate, the rotation happens around (0, 0) of the new (blue) grid - which is point (100, 60) in the original space. If you thought "the shape moved," you'd expect rotation around the shape's centre. But if you think "the grid moved," you correctly predict: the rotation pivots around the new grid's origin.
Chaining example: where the models diverge
Consider transform="translate(120, 80) rotate(45)" applied to a rect at (20, 15). What do the two mental models predict?
transform="translate(120, 80) rotate(45)" - Left: wrong model expects rotation around the rect's centre. Right: correct model shows the grid was translated (moving the origin to a new point), then the grid was rotated 45° around that new origin. The rect at local (20,15) swings with the grid. The two predictions produce visibly different results.
1. The Transform Functions
SVG's transform attribute accepts these functions:
| Function | Syntax | What it does |
|---|---|---|
translate |
translate(x [, y]) |
Shifts the coordinate system. If y omitted, defaults to 0. |
rotate |
rotate(angle [, cx, cy]) |
Rotates the coordinate system by angle degrees. Optional centre point (cx, cy) - defaults to (0, 0). |
scale |
scale(x [, y]) |
Scales the coordinate system. If y omitted, uniform scale (same as x). |
skewX |
skewX(angle) |
Skews along the x-axis by angle degrees. |
skewY |
skewY(angle) |
Skews along the y-axis by angle degrees. |
matrix |
matrix(a,b,c,d,e,f) |
Applies a full 2D affine transformation matrix. All others are shortcuts for specific matrices. |
An affine transformation is any transformation that preserves straight lines and parallelism. Lines stay straight, and parallel lines stay parallel. That's the guarantee.
What affine transformations can do (but don't have to):
- Change angles - skew turns a square into a rhombus (sides still parallel, corners no longer 90°)
- Change lengths - scale makes things bigger or smaller
- Move the origin - translate shifts everything
- Rotate - rotation preserves both angles and lengths, but it's still affine
What is not affine (and SVG's transform cannot do):
- Perspective - parallel lines converging to a vanishing point (available in CSS 3D, not in SVG transforms)
- Warping/bending - curving a straight edge (not available in either CSS or SVG transforms)
In practice: matrix(a,b,c,d,e,f) can express any combination of translate + rotate + scale + skew in a single operation. You'll rarely write it by hand - the named functions are far more readable - but they all collapse to this same 6-number matrix under the hood.
2. Translate
The simplest transform - shifts everything in the element's local coordinate system:
<svg width="300" height="150" viewBox="0 0 300 150">
<!-- Original position -->
<rect x="10" y="10" width="50" height="30" fill="#61afef"/>
<!-- Translated 100 right, 50 down -->
<rect x="10" y="10" width="50" height="30" fill="#e06c75"
transform="translate(100, 50)"/>
</svg>
Same rectangle, same coordinates - only the coordinate system is shifted.
Notice both rects have x="10" y="10". The red one appears elsewhere because its local origin has moved.
3. Rotate
Rotates the coordinate system by the given angle in degrees (clockwise, because y-axis points down):
<rect x="60" y="20" width="80" height="40" fill="#98c379"
transform="rotate(30)"/>
By default, rotate(angle) rotates around (0, 0) - the top-left corner of the current coordinate system. This is almost never what you want. To rotate around the element's own centre, use the three-argument form:
rotate(angle, cx, cy) - where (cx, cy) is the point to rotate around.
Red: rotated around (0,0) - swings far from its original position. Blue: rotated around its own centre (120,80) - stays in place.
4. Scale
Multiplies all coordinates by the scale factor:
<rect x="20" y="20" width="40" height="30" fill="#e5c07b"
transform="scale(2)"/>
<!-- Effective position: x=40, y=40, width=80, height=60 -->
Because transforms modify the coordinate system, scaling by 2 also doubles the element's x and y position - not just its size. A rect at (20, 20) with scale(2) effectively appears at (40, 40) at double size.
To scale "in place" (from centre), you need to translate to origin, scale, then translate back - or use CSS transform-origin.
The rect's position moved from (20,20) to (40,40) because the entire coordinate system was scaled.
scale(-1, 1) flips horizontally. scale(1, -1) flips vertically. This is how axis reversal works in practice - transforms are where it lives, not in the viewport/viewBox.
Skew - tilting the coordinate axes
skewX(angle) tilts the y-axis by the given angle, making vertical lines lean. skewY(angle) tilts the x-axis, making horizontal lines lean. The result is a parallelogram distortion:
Skew distorts the coordinate axes independently. skewX shears horizontally (vertical lines lean); skewY shears vertically (horizontal lines lean). Combined skews produce a diamond/rhombus shape from a square.
Scaling "in place" - the concrete technique
Say you have a rect at x="100" y="60" width="80" height="50" and you want to scale it to 1.5× from its centre. Its centre is at (140, 85) - that's x + width/2, y + height/2.
Approach 1: SVG transform attribute - translate the grid's origin to the centre, scale, then translate back:
<rect x="100" y="60" width="80" height="50" fill="#98c379"
transform="translate(140, 85) scale(1.5) translate(-140, -85)"/>
Reading right to left (the order things happen):
translate(-140, -85)- shift the grid so the rect's centre is now at (0,0)scale(1.5)- scale around (0,0) - which is the rect's centretranslate(140, 85)- shift everything back to the original position
Approach 2: CSS transform-origin - let the browser do the translate/untranslate for you:
<rect x="100" y="60" width="80" height="50" fill="#98c379"
style="transform: scale(1.5); transform-origin: 140px 85px;"/>
Same result, less manual maths. But remember: SVG's transform-origin defaults to (0, 0), so you must set it explicitly.
Note on units: because transform-origin is a CSS property (even when written in a style attribute), you must include units - 140px 85px, not 140 85. Unitless values are invalid CSS and will be ignored. The px here maps to SVG user units, so 140px in CSS = 140 in the SVG transform attribute.
Red: naive scale(1.5) - the rect grows and drifts (position scaled too). Green: the translate-scale-untranslate pattern - the rect scales from its centre and stays in place.
5. Chaining Transforms
You can apply multiple transforms in a single attribute, separated by spaces:
<rect transform="translate(100, 50) rotate(45) scale(1.5)"/>
Given translate(100, 50) rotate(45) scale(1.5):
| Reading direction | You're following… | How to think about it |
|---|---|---|
| Right to left | …the shape | "The shape is scaled, then rotated, then translated." More intuitive for simple cases. |
| Left to right | …the coordinate system | "The grid is shifted, then rotated, then scaled - now draw the shape on this modified grid." More faithful to what actually happens, and gives correct predictions when chaining gets complex. |
Both arrive at the same result. The left-to-right reading is more consistent with the core insight that transforms modify space, not shapes - and it's the one that won't mislead you when chains get long.
Either way, the critical point: translate(100,50) rotate(45) and rotate(45) translate(100,50) produce different results because the order of coordinate system modifications matters.
6. The <g> Element - Grouping
The <g> (group) element lets you apply a single transform to multiple children at once:
<svg width="300" height="200" viewBox="0 0 300 200">
<g transform="translate(50, 30) rotate(15, 100, 70)">
<rect x="60" y="40" width="80" height="60" fill="#61afef"/>
<circle cx="140" cy="70" r="20" fill="#e06c75"/>
<text x="60" y="120" font-size="12">Grouped!</text>
</g>
</svg>
Key points about <g>:
- It does not create a new viewport or coordinate system (unlike nested
<svg>). - It applies its
transformto all children - they inherit the modified coordinate system. - It can also carry presentation attributes (
fill,stroke,opacity, etc.) that children inherit. - It has no visual output of its own - it's purely structural.
- Children can have their own transforms, which stack on top of the group's transform.
Ghost shapes show original positions. The group moves and rotates them together as a unit.
<g> vs nested <svg>
From Lesson 1, recall that nested <svg> creates its own viewport and coordinate system. Here's when to use which:
<g>- when you want to move/rotate/scale a collection of shapes together within the current coordinate system.- Nested
<svg>- when you need an independent coordinate system with its own viewBox (e.g., embedding a reusable icon at a specific size).
7. transform-origin in SVG
CSS lets you set transform-origin to control the pivot point. In HTML, it defaults to 50% 50% (centre of the element). In SVG, the story is different:
For SVG elements (except the root <svg>), transform-origin defaults to the origin of the user coordinate system - not the centre of the element. This means CSS transforms like transform: rotate(45deg) will swing the element around the top-left corner unless you explicitly set transform-origin.
The rule: if an element participates in HTML layout, it gets HTML defaults (50% 50%). If it lives inside SVG coordinate space, it gets SVG defaults (0 0).
| Element | transform-origin default |
Why |
|---|---|---|
Root <svg> |
50% 50% (centre) |
Lives in HTML document flow - behaves like a <div> or <img> |
Nested <svg> inside <foreignObject> |
50% 50% (centre) |
<foreignObject> bridges back into HTML layout |
| Everything else (rect, circle, g, nested svg, etc.) | 0 0 (UCS origin) |
Lives inside SVG coordinate space |
Two approaches to rotate around an element's centre:
<!-- Approach 1: SVG transform attribute with explicit centre -->
<rect x="50" y="30" width="100" height="60"
transform="rotate(45, 100, 60)"/>
<!-- 100 = x + width/2, 60 = y + height/2 -->
<!-- Approach 2: CSS transform with transform-origin -->
<rect x="50" y="30" width="100" height="60"
style="transform: rotate(45deg); transform-origin: 100px 60px;"/>
The SVG attribute approach (Approach 1) is simpler for rotation because the centre point is built into the rotate() function. For CSS animations, you'll need to set transform-origin explicitly.
8. How Transforms Relate to the Coordinate System
Let's tie this back to Lesson 1. Remember the three layers:
- User Coordinate System - the infinite stage
- viewBox - selects a rectangle of the stage
- Viewport - physical size on the page
Transforms add a fourth layer: local coordinate systems. Each element with a transform attribute gets its own local coordinate system, derived from its parent's. These nest:
<svg viewBox="0 0 200 200"> <!-- UCS established by viewBox -->
<g transform="translate(50, 50)"> <!-- local system: shifted -->
<g transform="rotate(30)"> <!-- nested local: shifted + rotated -->
<rect x="0" y="0" width="40" height="40"/> <!-- drawn in shifted+rotated space -->
</g>
</g>
</svg>
The rect is at (0,0) in its own local space - but that local space has been translated and rotated relative to the UCS. The coordinate system chain is: UCS → translate → rotate → element.
This "transforms move the coordinate system" model applies to CSS transforms on HTML elements too - it's the same maths. The reason CSS rotations and scales on <div>s seem to "just work" centred is that CSS defaults transform-origin to 50% 50%. Under the hood, the browser is doing the translate-to-centre → apply transform → translate-back pattern automatically. SVG makes this machinery visible because it defaults to (0, 0).
Animating transforms
Transforms can be smoothly animated via three methods (covered in Lessons 9-11):
- CSS transitions - add
transition: transform 0.3s easefor hover/state changes - CSS @keyframes - for looping or multi-step animations
- SMIL
<animateTransform>- declarative animation baked into the SVG markup (no external CSS needed)
All three require you to set transform-origin explicitly for elements inside the SVG (because of the 0 0 default). We'll cover these properly in Lessons 9–11.
9. Interactive: Transform Explorer
Drag sliders to see how translate, rotate, and scale modify the coordinate system. The ghost shows the original position.
10. Reusability: <defs>, <symbol>, and <use>
Grouping with <g> organises elements visually, but SVG also has a system for defining reusable content that isn't rendered until you explicitly instantiate it.
<defs> - define without rendering
Content inside <defs> is invisible until referenced elsewhere. It's the standard place to put gradients, patterns, clip paths, filters - and any shapes you want to reuse:
<svg viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Defined but not rendered -->
<circle id="dot" r="10"/>
</defs>
<!-- Instantiate it multiple times with <use> -->
<use href="#dot" x="50" y="50" fill="coral"/>
<use href="#dot" x="150" y="50" fill="#61afef"/>
<use href="#dot" x="250" y="50" fill="#98c379"/>
</svg>
One circle defined in <defs>, instantiated three times with <use> at different positions and fills.
<use> - instantiate a clone
<use href="#id"> creates a visual clone of the referenced element. You can reposition it with x/y and override inheritable presentation attributes (like fill). The clone lives in a shadow-DOM-like scope - you can't directly style its internals with external CSS selectors.
| Attribute | Purpose |
|---|---|
href | Reference to the element to clone: #id or file.svg#id |
x, y | Position offset for the clone |
width, height | Override dimensions (only works on <symbol> or <svg> targets) |
<symbol> - reusable template with its own viewBox
<symbol> is like <defs> content but it has its own viewBox and preserveAspectRatio. When instantiated via <use>, the symbol scales to the width/height you give the <use> element. This makes it ideal for icon systems:
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="icon-star" viewBox="0 0 24 24">
<polygon points="12,2 15,9 22,9 16,14 18,21 12,17 6,21 8,14 2,9 9,9"/>
</symbol>
</svg>
<!-- Use at different sizes - the symbol scales to fit -->
<svg viewBox="0 0 300 80" xmlns="http://www.w3.org/2000/svg">
<use href="#icon-star" x="10" y="10" width="24" height="24" fill="coral"/>
<use href="#icon-star" x="50" y="5" width="36" height="36" fill="#e5c07b"/>
<use href="#icon-star" x="110" y="0" width="48" height="48" fill="#61afef"/>
</svg>
<defs> is a generic invisible container. <symbol> adds viewBox + preserveAspectRatio, so the content scales proportionally when used at different sizes. Use <defs> for gradients, patterns, clip paths; use <symbol> for reusable graphic components (icons, repeated motifs).
A <use> clone creates a shadow boundary. You can set inheritable properties on the <use> element (fill, stroke, font styles), and they'll cascade into the clone - but only if the original element doesn't set them explicitly. You cannot target internal parts of the clone with CSS selectors from outside. This is intentional - it preserves encapsulation.
Quiz: Check Your Understanding
Question 1
What does SVG's rotate(45) rotate around by default?
Question 2
Given transform="translate(50, 0) scale(2)" on a rect at x="10", where does the rect's left edge end up?
Question 3
What's the difference between <g> and nested <svg>?
<g> creates a new viewport, <svg> does not<g> inherits the parent coordinate system; nested <svg> establishes a new one<g> cannot have transforms appliedHands-On Exercise
Create an SVG that demonstrates transform chaining and grouping:
- Create an SVG with
width="400" height="300" viewBox="0 0 400 300". - Draw a rectangle at
x="0" y="0" width="60" height="40"with any fill. Applytransform="translate(170, 130)"so it appears near the centre. - Now add
rotate(45, 30, 20)to the transform (after the translate). Predict: will it rotate around the centre of the rect or around (30,20) in the translated coordinate system? (Hint: both are the same here - 30 = width/2, 20 = height/2, and these are in the local translated space.) - Wrap two shapes in a
<g>withtransform="translate(100, 50)". Then give one of the children its owntransform="rotate(20, 30, 20)". Observe how the child's transform stacks on top of the group's.