The language

Syntax overview

You write the CSS of one cell. css-doodle runs it for every cell and emits ordinary CSS. Directives and functions start with @ or $.

Statements

A doodle is a list of statements, like the body of a CSS rule. A { before ; makes a block; otherwise it is a declaration.

Declarations

A property and a value. Names that start with @ are directives: css-doodle reads them and does not emit them as CSS. Every other property applies to every cell.

@grid @size @place @gap @shape @content @seed @use

Selector blocks

Style the component, the grid, or an element inside each cell. :doodle and :container are generated once, not once per cell. A selector that begins with a colon attaches to the cell; any other is a descendant.

:doodle :container :hover span

Conditional blocks

Style only the cells that match. @media keeps its CSS meaning. @keyframes is hoisted.

@cell(…) @nth(…) @random

Blocks can nest. Comments are /* … */. As in CSS, the last declaration before } does not need a semicolon.

@grid and background are declarations, :doodle and :hover are selector blocks, and @cell is a conditional block.

:doodle {
  @grid: 6 / 60vmin _1px;
}

background: #eee;
transition: .2s;

@cell(random .3) {
  background: #000;
}

:hover {
  background: #e6437d;
  scale: 1.2;
  z-index: 40;
}

Functions

A function sits where a CSS value would. css-doodle evaluates it again for each cell, so @i is that cell’s index and @r(…) can be a different random value in every cell.

@name(arguments)
A doodle function, evaluated once per cell.
$(expression)
Calc. $(1 + 2) is 3.
$px(expression)
Calc, then a unit. $px(@i * 10) is a length.

Optional parentheses

Drop the parentheses when there are no arguments. @i means @i().

A number after the name

A number stuck to the function name becomes the first argument. Math names that already end in a digit, such as @log2, are left alone.

You writeIt means
@i5@i(5)
@m5(…)@m(5, …)
@r360@r(360)
@p1-2(…)@p(1-2, …)
@doodle200(…)@doodle drawn at 200 pixels

Composition

A dot chains functions. The rightmost one runs first: @p.cycle(a b c) is @p(@cycle(a b c)). The inner @ is optional.

@grid: 5 / 60vmin _2px;
background: #000;
border-radius: @p.cycle(100% 0 0 0);

Units

A unit on an argument is kept on the result. @r(10px) and @r(10)px both produce lengths. A leading operator on a cell function is a modifier: @i(*10px), @x(-1), @t(%360deg).

Arguments

Separate arguments with commas. If an argument contains a comma, wrap it in parentheses or quotes.

  • In pick functions, [a-z] and [0-9] expand to a list of values.
  • ±1 becomes two arguments, -1 and 1: @r(±10deg).
  • --name reads that custom property when the doodle is generated.
  • A backtick is a double quote, so a doodle can live in a double-quoted HTML attribute.

Cell selectors

A conditional block runs only on the cells it matches. @cell can express any of the tests below; the other names are shorter spellings. Several arguments mean "any of these". not before the parentheses inverts the test.

SelectorPicks
@cell An index, a formula, an expression, or a random pick @cell(1, 3n, x = y, random .2)
@nth Cells by index, like :nth-child @nth(2n + 1)
@at The cell at a column and row @at(2, 3)
@row, @col A whole row or column @row(even) @col(1)
@even, @odd A checkerboard
@random Cells at random. A value below 1 is a probability. @random(.3)
@match Cells where an expression is true @match(dr < 3)

The first block paints both diagonals. The second rounds every cell that is not near the center.

@grid: 9 / 60vmin _1px;
background: #eee;

@cell(x = y, x + y = 10) {
  background: #000;
}
@cell not (dr < 3) {
  border-radius: 50%;
}

Expressions

Use $(…) when a value needs arithmetic or a comparison. The same language is accepted in @cell, @match, @random, and @shape.

RuleExample
Everything is a number. Units are dropped.--w: 10px reads as 10
Comparisons give 1 or 0.x = y
Values next to each other multiply.2x, , (1 + 2)(3)
Custom properties drop the -- prefix.--n: 12 is n
Unknown names, missing operands, and cycles are 0.foo is 0

JavaScript math names work, plus gcd(a, b) and match(cond, a, b). Chained calls run right to left: tan.cos.sin(x).

Operators

Highest precedence first:

OperatorsMeaning
!not
^ **power
* / %multiply, divide, remainder
+ -add, subtract
<< >>shift
&bitwise and
|bitwise or
< > <= >= = == != compare
&&and
||or

Cell variables

Inside a cell selector, these names are available as well as custom properties:

NameValue
x, ycolumn and row, starting at 1 in the top-left
X, Yhow many columns and rows the grid has
i, Ithis cell’s number (from 1), and the total number of cells
dx, dyoffset from the center, in columns and rows
dr, dc, dmdistance from the center: straight-line, square, and city-block
da, dbangle from the center, in radians, and distance to the edge
randoma random number between 0 and 1

Each ring’s size and hue come from $(…) and the cell index @i. n is the custom property --n.

@grid: 1x12 / 60vmin;
--n: 12;
@place: center;
@size: $%(100 - 6 * @i);
border: 2px solid hsl($(360 / n * @i), 70%, 55%);
border-radius: 50%;

Embedded languages

These functions take a body of their own, then compile it to an image or a shape. You use the result as a CSS value, usually on background, clip-path, or @content.

@doodle

A nested doodle, rendered to an image, or inserted as an element with @content.

The body is ordinary doodle code. Digits on the name set the pixel size of the image.

background: @doodle( ... );

@svg

An SVG image for background, mask, or @content.

Blocks are elements, declarations are attributes. circle*8 repeats the tag.

background: @svg( ... );

@shape

A polygon() for clip-path.

A preset name, or polar commands in the angle t.

clip-path: @shape( ... );

@pattern

A pixel pattern drawn on the GPU, when a real grid would be too heavy.

A virtual grid, a fill, and match rules that pick a color.

background: @pattern( ... );

@shaders

A WebGL image from GLSL, for background or @content.

Fragment source, or named sections. texture { } can hold a nested doodle.

background: @shaders( ... );

The body still uses name: value and name { }. Each language decides what a name and a value mean.

@grid: 1 / 60vmin;
background: @svg(
  viewBox: 0 0 18 16;
  circle*8 {
    fill: #000;
    r: .7;
    cx: @n(*2);
    cy: 8;
  }
);

Error recovery

Incomplete input still produces a doodle. css-doodle reports each of these once per component, in the console and on the diagnostics property.

What you wroteWhat happens
An unknown property or at-ruleEmitted as written
An unknown function with argumentsEmitted as its name, @name
An unclosed (The value swallows the declarations that follow

Going further