Home

Experimenting A New Syntax To Write SVG

27 September 2022

It's been a while since I introduced a new syntax to the css-doodle project to solve my own problems. While I'm not sure it would be useful elsewhere, I need to make some notes before I forget the details.

Motivation

I always find it a bit hard to write SVG without the help of additional tools or libraries. The main issue for me is that SVG code grows too fast to keep up. If we take a closer look at the way most SVG is written, you'll notice that the code grows in two dimensions: attributes and tags.

ATTRIBUTES TAGS

From this perspective, it might explain why writing HTML is relatively easier than SVG. There are much fewer attributes in HTML elements so the code of HTML grows approximately by tags in ONE dimension.

HTML could have become like SVG if CSS did not exist. We can see this from several deprecated attributes such as bgColor, align, border, etc.

<div bgColor="#ff0"></div>

Nowadays, though, it's still possible for HTML to grow in two dimensions if using tools like Tailwind, where the long predefined class names are kind of extended attributes to HTML elements.

CSS Syntax

Fortunately, not all SVG attributes are only written in tags. The presentation attributes can also be placed in style sheets as normal CSS properties.

path {
  stroke: rebeccapurple;
  stroke-dasharray: 5 1 5;
}

What I love about CSS is its readability. There are no unnecessary quotes around property values even when separated by spaces. And the CSS code always expands from top to bottom if following best practices.

What if I could write all SVG attributes in CSS?

Despite the fact it's unlikely to change the two-dimensional nature of SVG code to one-dimensional at the same time to make it isomorphic transforming back and forth, it would at least be pleasant to write SVG with the syntax of CSS.

svg {
  viewBox: 0 0 10 10;
}

This seems to work. Tags are written as CSS selectors and attributes as CSS properties. For child SVG elements we can use the upcoming CSS nesting syntax. However, I'd prefer the SCSS way of nesting because I don't have the same concerns with browser engines as described in this article.

svg {
  viewBox: 0 0 100 100;
  path {
    d: M 50 1 1 50 50 99 99 50 z;
    stroke: rebeccapurple;
  }
}

Parsing Challenges

The process of converting from SVG syntax to CSS did not go as smoothly as expected due to the various formats of SVG attribute names and values. Therefore, I had to extend the CSS syntax a bit.

1. Colon-separated attributes

<use xlink:href="#circle" />

Since CSS uses the colon symbol as the separator in a declaration, it must be handled specially if the attribute name also contains a colon character.

use {
  xlink:href: #circle;
}

2. Semicolons in SVG animation

<animate value="25,20,50,75; 45,55,50,75" />

In CSS, a semicolon is used to mark the end of a declaration. While SVG uses semicolons to group animation values. How can you tell the difference between the two types of semicolons when they are put together?

animate {
  value: 25,20,50,75; 45,55,50,75;
}

3. Text

<text>hello world</text>

There is no way to specify a text node in CSS property, so I need to define a new one to hold the content of the text node as respect to pseudo-element in CSS.

text {
  content: hello world;
}

4. Inline styles

<circle style="animation: move .2s; fill: red" />

Using the style keyword to represent each CSS property is the best way I could find to convert inline styles. They are normally not encouraged to use so it seems reasonable to make them a bit more verbose.

circle {
  style animation: move .2s;
  style fill: red;
}

This is how the code for a simple graph will look like:

svg {
  viewBox: 0 0 10 10;
  stroke: black;
  stroke-width: .1;
  rect {
    x: 8;
    y: 8;
    width: 1;
    height: 1;
    fill: yellow;
    style transform-box: fill-box;
    transform-origin: center;
    transform: rotate(45)
  }
  circle {
    fill: deepskyblue;
    cx: 4;
    cy: 4;
    r: 3
  }
}

Some New Features

As I experimented further, I realized that several new features would make the new syntax more user-friendly.

1. Attribute Destructuring

Some attributes can logically be grouped in pairs, so it is convenient to place them on the same line, separated by commas.

/* <rect width="50%" height="50%" /> */
rect {
  width, height: 50%;
}

/* <circle cx="4" cy="5" /> */
circle {
  cx, cy: 4 5;
}

This shorthand also makes it possible to use it with the @plot function in css-doodle which returns a pair of coordinate values.

circle {
  cx, cy: @plot(r: 4);
}

2. Id Abbreviation

The id attribute is for referencing element and is often used in gradients, markers, and filters. Instead of writing it inside blocks, it can also be set as an ordinary CSS id selector of the element.

filter#my-filter {

}

/* is equivalent to */

filter {
  id: my-filter;
}

3. Inline defs

In SVG, graphic objects are stored within the <defs> element and to be use somewhere else by referencing their ids. But sometimes I find it quite neat to define them directly where they are to be used. The reference ids are then generated automatically.

circle {
  fill: defs linearGradient {}
}

/* is equivalent to */

defs {
  linearGradient#my-gradient {}
}
circle {
  fill: url(#my-gradient);
}

4. Multiplication

Inspired by Emmet, the operator * is used to generate the same element multiple times.

circle*3 {

}

/* is equivalent to */

circle {}
circle {}
circle {}

It'll be most effective when combined with css-doodle functions.

svg {
  viewBox: 0 0 10 10;
  stroke: #000;
  stroke-width: .1;
  path*3 {
    fill: none;
    d: @pn(M 5 10 Q 5 5 8 1,
           M 5 10 Q 4 5 2 4,
           M 5 10 Q 6 6 8 4);
  }
  circle*3 {
    fill: @pn(yellow, blueviolet, deeppink);
    cx, cy: @pn(8 1, 2 4, 8 4);
    r: .5;
  }
}

Here's an example to draw a symmetrical structure.

svg {
  viewBox: -50 -50 100 100;
  path*6 {
    transform: rotate(@calc(@n*60));
    stroke-linecap: round;
    stroke: black;
    d: M 0 0 0 44
       @M2x3(M 0 @calc(-9*@ny)
             L @pn(±9) @calc(-10*@ny - 5.8));
  }
}

More examples

Earlier this year, I tweeted a code example that used this new syntax, as a reply to rewrite Shunsuke Takawo's Processing program in css-doodle. The multiplication operator had not yet been added at that time.

Now the SVG part can be simplified as this: (CodePen)

svg {
  viewBox: -57 -57 114 114;
  stroke-width: .15;
  stroke: #000;
  fill: #fff;
  circle*720 {
    r: @calc(sin(π/@N*@n)*10);
    cx, cy: @Plot(
      r: 40+sin(4t)*5+sin(12t)*5
    );
  }
}

I shared another sample on Twitter that was generated using SVG paths and filters. I did not attach the code because I thought it was not as elegant as its output. Still, it would be a good example to recreate it with css-doodle functions under the new syntax.

First, outline the structure by placing each Q coordinate around a circle.

svg {
  viewBox: 0 0 100 140;
  g {
    fill: none;
    stroke: black;
    path*20 {
      d: M 70 50
         Q @Plot(r: 60; move: 20 80)
           @Plot(r: 125; move: 80 60);
    }
  }
}

Then add more paths and random stroke styles. Not as extactly as the old one but you get the idea. (CodePen)

svg {
  viewBox: 0 0 100 140;
  g {
    fill: none;
    path*200 {
      stroke: @p(
        #f9f8eb, #76b39d, #05004e, #fd5f00
      );
      stroke-width: @r2;
      stroke-dasharray: @M2.@r(10, 60);
      d: M 70 50
         Q @Plot(r: 60; move: 20 80)
           @Plot(r: 125; move: 80 60);
    }
  }
}

How To Use It in Your Own Project?

The latest version of css-doodle offers the generator as a function that returns the output SVG code.

import { svg } from 'css-doodle/generator';

let code = svg(`
  viewBox: 0 0 10 10;
  circle {
    cx, cy, r: 5
  }
`)

// output:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
  <circle cx="5" cy="5" r="5"></circle>
</svg>

PostCSS

The plugin postcss-doodle allows you to use @svg function in regular CSS.

div {
  background: @svg(
    viewBox: 0 0 10 10;
    circle { cx, cy, r: 5 }
  );
}

Limitations

The conversion isn't complete. Dealing with SVG elements like <foreignobject> can be difficult. I haven't tested it with long SVG code, but I think it'll also be bloated as the drawing gets complex.

I like this new syntax mainly because it has less visual noise compared to the original SVG. For me the most satisfying thing is that it integrates so well with css-doodle.

Playground

https://css-doodle.com/svg

Related post: https://yuanchuan.dev/polygon-shapes