Function

@svg

Creates an SVG image from XML or a CSS-like syntax. The result is a url() for background, mask or border-image, and an inline element when used with @content.

Syntax

  • @svg(<svg-body>)
  • @svg(<svg ...>...</svg>)
svg-body
CSS-like rules. Top-level declarations become attributes of the <svg> root; blocks become its children. Wrapping the body in svg { } is optional. Blocks for script, iframe and other tags that run script or load a page are left out, and so are on… event attributes and javascript: values. Markup inside content: is kept as written.
XML
Used as written when the body starts with <; nothing is removed. xmlns is added if it is missing, and a fragment such as a lone <circle/> is wrapped in an <svg>.

A simple shape

Write an element as a block, with its attributes inside. Here, circle creates a <circle>, and cx, cy: 5 sets both coordinates to 5. Declarations outside the block belong to the SVG itself.

viewBox: 10 is short for viewBox: 0 0 10 10, an area 10 units wide and 10 units high, and viewBox: 10 20 sets the width and height separately.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: 10;
  circle {
    cx, cy: 5;
    r: 4;
    fill: #000;
  }
);

The same image as XML:

@grid: 1 / 60vmin;
background: @svg(
  <svg viewBox="0 0 10 10">
    <circle cx="5" cy="5" r="4" fill="#000"/>
  </svg>
);

Repeating elements

Add * after an element name to repeat it: circle*10 makes 10 circles. Use @n for the current step, starting at 1. Like @m, repetition also accepts a range (1-5), a grid (16x16) or a function such as circle*@ri(3, 8).

This grid uses @nx and @ny for each line’s coordinates, and @p to pick its direction. The padding .2 on the viewBox, or p .2 for short, adds space around the edges so the strokes are not clipped.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: 0 0 16 16 padding .2;
  stroke: #000;
  stroke-width: .4;
  stroke-linecap: round;
  line*16x16 {
    x1, y1, x2, y2: @p(
      @nx(-1) @ny(-1) @nx @ny,
      @nx @ny(-1) @nx(-1) @ny,
      @nx @ny(-1) @nx @ny
    );
  }
);

Functions work inside @svg too. Here, each circle’s position, size, and color depend on @n, forming a Fibonacci spiral.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: -50 -50 100 100;
  circle*300 {
    fill: hsl($(120-90*@sin.n), 80%, 50%);
    r: @sqrt(@n/60);
    cx: $(@n*.618^4 * cos(2π*@n*.618));
    cy: $(@n*.618^4 * sin(2π*@n*.618));
  }
);

Conditions

A @match(test) { … } block keeps its body only where the test holds. Its elements are added where the block stands, and its declarations set attributes of the element around it.

The test is evaluated like $(), so it reads variables, and the step of a repeat as n, nx and ny or @n, @nx and @ny. Here the dots on both diagonals turn pink and get a ring.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: 0 0 10 10;
  g*5x5 {
    transform: translate($(@nx * 2 - 1) $(@ny * 2 - 1));
    circle { r: .5 }
    @match(@nx == @ny || @nx + @ny == 6) {
      fill: #e6437d;
      circle {
        r: .8;
        fill: none;
        stroke: #000;
        stroke-width: .15;
      }
    }
  }
);

As a value, fill: @match(…) is the @match function.

Joining shapes

union, subtract, intersect and exclude blocks join the shapes inside them into one path, which takes the attributes of the block. subtract takes the rest away from the first shape, and exclude keeps what an odd number of them cover, as the groups of @shape do. smooth: r rounds the corners where shapes meet:

@grid: 1 / 60vmin;
background: @svg(
  viewBox: -5 -5 10 10;
  subtract {
    fill: #000;
    smooth: .3;
    circle { r: 4.5 }
    circle*8 { r: 1.1; cx, cy: @Plot(r: 4.5) }
    circle { r: 1.5 }
  }
);

grow: d moves the joined outline out by d, or in when it is negative. Here the pink copy is the black one grown by .8:

@grid: 1 / 60vmin;
background: @svg(
  viewBox: -5 -5 10 10;
  union*2 {
    fill: @pn(#e6437d, #000);
    grow: @pn(.8, 0);
    circle*5 { r: 1.2; cx, cy: @Plot(r: 2) }
  }
);

path, polygon, polyline, circle, ellipse and rect can be joined, and a g inside counts as the union of its shapes. Circles, arcs and rounded corners stay round, and other curves become short lines. The shapes' own transform, paint and animations are dropped, while animate, draw and title on the block go to the path.

Gradients and definitions

A gradient can go directly in fill. It is placed in <defs> and given an id; the fill becomes url(#id). There is no need to write either yourself.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: 0 0 10 10;
  rect {
    width, height: 10;
    fill: linearGradient {
      gradientTransform: rotate(45);
      stop { offset: 0; stop-color: #000 }
      stop { offset: 1; stop-color: #e6437d }
    }
  }
);

The same syntax works for patterns, filters, clip paths, masks, markers, and symbols. An explicit defs is optional: fill: defs linearGradient { … } works too. For href and xlink:href, the reference is #id rather than url(#id). For a shorter way to write gradients, see @linearGradient and @radialGradient.

Other elements written as a value, such as href: path { … } in a textPath, also get an id, but go to the top level of the SVG, so they are drawn as well.

Inline SVG

Use @content instead of background to put the SVG element inside the cell, rather than use it as a background image.

@grid: 1 / 60vmin auto;
@content: @svg(
  viewBox: -5 -5 10 10;
  circle*12 {
    r: @nN(*4);
    fill: none;
    stroke: #000;
    stroke-width: .15;
  }
);

Animation

draw: 3s infinite draws a shape’s stroke over three seconds, then repeats. Set a stroke to make it visible. This works on paths, lines, circles, ellipses, rectangles, polygons, and polylines.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: 0 0 10 10 p 2;
  path {
    stroke: #000;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-width: .4;
    fill: none;
    draw: 3s infinite;
    d: M 0 0
       @M10.pn(0 @n @n 0, @n 0 0 @n)
       @M10.pn(@n @N @N @n, @N @n @n @N);
  }
);

To animate an attribute, write animate followed by its name. Separate the values with semicolons, then put the timing after /. These circles grow from a radius of 0.2 to 0.8 and shrink back over two seconds.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: -5 -5 10 10;
  circle*8 {
    cx, cy: @Plot(r: 3);
    r: .6;
    fill: #000;
    animate r: .2; .8; .2 / 2s ease infinite;
  }
);

The timing, for draw too, is made of these parts. For example, 2s 1s 3 waits one second, then runs three times for two seconds each.

TimingMeaning
2s, 500ms, 1min, 1hThe first time is the duration, the second the delay. A number on its own, such as 2, is a duration in seconds.
3Next to a duration, the repeat count.
infinite, indefiniteRepeats forever.
ease, ease-in, ease-out, ease-in-out, cubic-bezier(…)The easing between each pair of values. The default is linear.
discrete, step-endJumps from one value to the next.
pacedMoves at an even speed across all the values.
forwards, freezeKeeps the final value.

A single value is the value to animate to. With no attribute name, animate is the same as draw.

animate transform makes an SVG transform animation. The first word of the values is the transform type, such as rotate or scale:

@grid: 1 / 60vmin;
background: @svg(
  viewBox: -6 -6 12 12;
  fill: none;
  stroke: #000;
  stroke-width: .2;
  rect*4 {
    x, y: $(-@n);
    width, height: $(@n * 2);
    animate transform: rotate 0; 90 / @n(*2)s infinite;
  }
);

Other shortcuts

SyntaxMeaning
g { circle { … } }A circle inside a group. g circle { … } and g > circle { … } do the same.
circle, rect { … }Two elements with the same attributes.
feMergeNode;An empty element. Right after a declaration a bare name is one more value of it, as in values: 1; 2; 3;, so write circle { } there.
x, y: 1, 2;Sets x="1" and y="2". A single value sets both attributes, and is evaluated once: x, y: @r(10) gives both the same number.
rect#a.b { … }A rectangle with id="a" and class="b".
circle#a { … } twiceBlocks with the same name and id inside the same parent make one element.
content: hello;Text inside the element, as in a text block. Quotes around it are removed, and markup such as <tspan> is kept.
style { circle { … } }A stylesheet inside the SVG. Its contents are ordinary CSS. style circle { … } is short for it.
style fill: red;An inline style. Use style: { … } for several CSS declarations.
--r: 3;A variable, read as $r or in $(). It applies from where it is written on, blocks included, and is set again in each copy of a * repeat.
--c, fill: @p(…);Sets the variable and the attribute to one value, picked once. Later declarations read it as $c.
d: L 2 2 …Path data that starts with L or l starts with M instead, so @arc data can begin a path.
$dots()Inserts the body of a user-defined function, which can hold elements and * repeats.
--dots(): circle*9 { … }A function defined inside the @svg, in any block. Only that @svg sees it, and there it hides a doodle function of the same name.

Element and attribute names are case-insensitive, so viewbox works like viewBox. xlink: and xml: attributes, such as xlink:href, work as written.

In JavaScript

The same code gives SVG markup without a doodle through svg() from css-doodle/generator.

See also