SVG from Scratch

SVG is not an image format. It's a coordinate system, a styling surface, and an animation target — all in one. Here's what you actually need to know to use it well.

SVG clicked for me the day I stopped thinking of it as a file format and started thinking of it as a DOM. Every shape is an element. Every attribute is a style. You can select it with CSS, animate it with transitions, and target it with JavaScript — the same way you work with any other HTML element.

Most people learn SVG backwards: they export it from Figma, squint at the generated markup, clean up the IDs, and move on. That works, but you miss the part where SVG is actually fun to write by hand.

The coordinate system

Every SVG has a viewBox — a coordinate space that maps to whatever size the element renders at in the browser. This is the thing most explanations skip, and it’s the thing that makes everything make sense.

<svg viewBox="0 0 200 100" width="400" height="200">

viewBox="0 0 200 100" means: my internal coordinate space is 200 units wide and 100 units tall, starting at 0,0 in the top-left. The width="400" height="200" means: render it at 400×200px in the browser. The browser scales the coordinate space to fit — 1 SVG unit = 2px here.

The practical consequence: you can design in whatever coordinate space feels natural (I usually use 0 0 100 100 for things that need percentages, or 0 0 24 24 for icons), and the browser handles the scaling.

viewBox — 100×100 coordinate space, rendered larger
50,500,0100,100
<svg viewBox="0 0 100 100" width="200" height="200">
  <circle cx="50" cy="50" r="40"
    fill="none" stroke="purple" stroke-width="1"/>
</svg>

Basic shapes

Five elements cover most of what you’ll need:

<circle>cx, cy for center, r for radius.

circle
<svg viewBox="0 0 100 60" width="200" height="120">
  <circle cx="50" cy="30" r="22"
    fill="rgba(136,74,255,0.2)"
    stroke="#884AFF"
    stroke-width="1.5" />
</svg>

<rect>x, y for top-left corner, width, height. Add rx for rounded corners.

rect with rounded corners
<svg viewBox="0 0 100 60" width="200" height="120">
  <rect x="15" y="10" width="70" height="40" rx="8"
    fill="rgba(233,51,48,0.15)"
    stroke="#E93330"
    stroke-width="1.5" />
</svg>

<line>x1,y1 to x2,y2. Needs a stroke to be visible.

<polygon> — a closed shape from a list of x,y points.

polygon — triangle
<svg viewBox="0 0 100 80" width="200" height="160">
  <polygon points="50,10 90,70 10,70"
    fill="rgba(136,74,255,0.15)"
    stroke="#884AFF"
    stroke-width="1.5"
    stroke-linejoin="round" />
</svg>

<path> — the most powerful and the most intimidating. Every other shape can be expressed as a path.

The <path> element

Path data lives in the d attribute. The basic commands:

CommandMeaning
M x,yMove to (starts a new subpath)
L x,yLine to
H xHorizontal line to
V yVertical line to
C x1,y1 x2,y2 x,yCubic bezier curve
A rx,ry rot large-arc sweep x,yArc
ZClose path (line back to start)

Uppercase = absolute coordinates. Lowercase = relative to current position.

path — arrow
<svg viewBox="0 0 100 60" width="200" height="120">
  <path
    d="M 20,30 L 70,30 M 55,15 L 70,30 L 55,45"
    fill="none"
    stroke="#884AFF"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round" />
</svg>

Styling with CSS

SVG elements are styled with a mix of SVG-specific presentation attributes and standard CSS. The CSS always wins over presentation attributes — which means you can override exported SVG styles with a stylesheet.

Properties that work like CSS: fill, stroke, stroke-width, opacity, transform, font-size, font-family.

Properties that are SVG-only: stroke-linecap, stroke-linejoin, stroke-dasharray, stroke-dashoffset.

/* Target SVG elements from a stylesheet */
.icon circle {
  fill: var(--brand);
  transition: fill 0.3s ease;
}

.icon:hover circle {
  fill: var(--brand-light);
}

stroke-dasharray — the useful trick

stroke-dasharray defines a pattern of dashes and gaps along a stroke. Set it to the total length of the path and combine with stroke-dashoffset to animate drawing:

stroke-dasharray — dashed border
<svg viewBox="0 0 100 60" width="240" height="144">
  <rect x="10" y="10" width="80" height="40" rx="6"
    fill="none"
    stroke="#884AFF"
    stroke-width="1.5"
    stroke-dasharray="8 4" />
</svg>

And the animated draw-on effect — probably the most-used SVG trick in production:

stroke draw-on animation
.draw-path {
  stroke-dasharray: 220;
  stroke-dashoffset: 220;
  animation: draw 2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

@keyframes draw {
  to { stroke-dashoffset: 0; }
}

stroke-dasharray sets the dash length to the full path length, so the path looks like a single dash. stroke-dashoffset shifts that dash off-screen. Animate stroke-dashoffset to 0 and the path draws in. To get the exact path length: pathElement.getTotalLength() in JavaScript.

<defs> and reusable shapes

<defs> stores shapes, gradients, and filters that aren’t rendered directly — they’re referenced by ID elsewhere. This is how you avoid repeating markup.

linearGradient inside defs
<svg viewBox="0 0 100 60" width="240" height="144">
  <defs>
    <linearGradient id="brand-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
      <stop offset="0%" stop-color="#884AFF" />
      <stop offset="100%" stop-color="#E93330" />
    </linearGradient>
  </defs>

  <rect x="10" y="15" width="80" height="30" rx="6"
    fill="url(#brand-gradient)" />
</svg>

Gradients, clip paths, masks, and filters all live in <defs>. The url(#id) syntax is how you reference them from any element in the same document.

<clipPath> — masking with shapes

A <clipPath> defines a shape that clips another element — anything outside the clip shape is hidden.

clipPath — circle crop
<svg viewBox="0 0 100 60" width="240" height="144">
  <defs>
    <clipPath id="circle-crop">
      <circle cx="50" cy="30" r="22" />
    </clipPath>
  </defs>

  <g clip-path="url(#circle-crop)">
    <!-- anything here is clipped to the circle -->
    <image href="photo.jpg" x="0" y="0" width="100" height="60" />
  </g>
</svg>

Clip paths are how you do circular avatars, custom-shaped containers, and reveal animations in SVG. The clipping shape can be any SVG element — not just circles.

Where to go from here

The four things that unlock most SVG work:

  1. getTotalLength() — measure any path for draw-on animations
  2. <use> — reference a defined shape multiple times with different positions and transforms
  3. CSS transform-origin — rotation and scaling need an origin; in SVG it defaults to 0,0 (top-left), which is almost never what you want. Set it explicitly.
  4. preserveAspectRatio — controls how an SVG scales when the aspect ratio of the element doesn’t match the viewBox. xMidYMid meet is the default (letterbox). none stretches to fill.

The <path> d attribute looks intimidating but you only need M, L, C, and Z for 90% of custom shapes. The rest you can generate from Figma and edit by hand when needed.