Guide

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. As in CSS, a space before the colon matters: span:hover and span :hover differ.

:doodle :container :hover span

Conditional blocks

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

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

Function definitions

A --name(…): at the top level defines a function of your own and emits nothing. $name(…) calls it.

--twice(x): $(x * 2); $twice(3)

Blocks can nest. Comments are /* … */; // comments work only inside the bodies of @pattern, @scene, @shaders, @shape, @plot and @tile. As in CSS, the last declaration before } does not need a semicolon. A single-line at-rule such as @import url(fonts.css); is emitted as written at the top level, and dropped inside a block.

@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.
$name(arguments)
A function defined with --name().

Optional parentheses

Drop the parentheses when there are no arguments. @i means @i(). With arguments, the ( must touch the name: @r (5) is @r followed by the text (5).

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)
@r1.5@r(1.5)
@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. When the function on the left has a variant of that name, the dot calls the variant instead: @plot.scatter(star) is @plot's scatter form. Write .@name to always compose.

@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.
  • In @p and @pick, a colon in the first argument ends an index: @pick(@i: red, blue) alternates, and @p(@i: @palette(5)) goes through the palette.
  • ±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. A pseudo right after a selector applies to the cell, as in @nth(1)::after, and a comma lists several selectors: @at(1, 1), @at(2, 2).

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, @depth A whole row, column or depth @row(even) @col(1)
@even, @odd A checkerboard
@random Cells at random. Below 1 it is a probability, 1 or more a count. @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. Custom properties drop the -- and any unit.--w: 10px is w, read as 10
Inside a call, deg and turn are angles in radians.cos(60deg) is .5
A % right after a number is a percent sign, unless an unsigned number, a name or ( follows it.@tile.y > 50, 25%-5 is 20, 7%3 is 1
Comparisons give 1 or 0, and ignore a difference below 1e-9.x = y, .1 + .2 = .3 is 1
Values next to each other multiply.2x, 2π, (1 + 2)(3)
A power binds before a product and a sign, but a variable keeps its own sign.-2^2 is -4, 2x^2 is 2(x^2); with --k: -2, k^2 is 4
Numbers can be hexadecimal.0xff is 255
A dashed name is one variable if it is declared, otherwise a subtraction.a-b reads --a-b
&& and || return the operand that decides.0 || 5 is 5
Unknown names, missing operands, and cycles are 0.foo is 0
Division by zero gives Infinity.1 / 0

JavaScript math names work, plus gcd(a, b) and the GLSL math mod, fract, clamp, mix, step, smoothstep, radians and degrees, which read as in GLSL: mod(-1, 4) is 3. Chained calls run right to left: tan.cos.sin(x).

match(test, value, test, value, …, else) gives the value after the first test that holds, else the last odd argument or 0. pick(k: a, b, …) gives item k from 1, wrapping around: pick(4: a, b, c) is a. Both evaluate only the value they return.

Operators

Highest precedence first:

OperatorsMeaning
!not
^ **power
2xvalues next to each other multiply
- + before a valuesign
* / ÷ %multiply, divide, remainder (with the sign of the left side)
+ -add, subtract
<< >>shift
&bitwise and
|bitwise or
< > <= >= = == != ≤ ≥ ≠compare
&& ∧and
|| ∨or

Live values

@t, @ux, @uw and the other values that follow time, the pointer or the doodle's size are CSS variables, so they have no number when the doodle is generated. The math around them is written out as CSS math instead: $(@t / 10)deg becomes calc((var(--cssd-utime) / 10) * 1deg).

Arithmetic, comparisons, logic, match(), pick(k: …), the GLSL math and the common Math functions carry over. Shifts, bitwise operators, gcd() and the other Math functions have no CSS form, so with a live value they give 0. What is decided when the doodle is generated cannot wait for a live value. @match, @cell and the @match function read it as 0, and elsewhere it is dropped, so @r(@t, 5) is @r(5).

Cell variables

Cell selectors, @match, the index of @p and $() read these names as well as custom properties. When a custom property has the same name, $() reads the property and the others read the cell variable.

NameValue
x, ycolumn and row, starting at 1 in the top-left
X, Yhow many columns and rows the grid has
z, Zdepth level and number of levels, in a grid such as 1x1x8
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: 12x1 / 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, or a shape() when corners are rounded.

A preset name, or commands that draw a curve in the angle t (polar r, or x and y), place points, or join shapes in groups.

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.

@content: @pattern( ... );

@scene

A 3D model drawn on the GPU, for background or @content.

Shapes such as sphere and box, in the language of @shape.

background: @scene( ... );

@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 the problems below once per component, in the console and on the diagnostics property.

What you wroteWhat happens
An unknown property or at-ruleEmitted as written, without a warning
An unknown function with argumentsEmitted as its name, @name
An unclosed (The value swallows the declarations that follow
A @use variable that includes itselfSkipped the second time
A function that calls itselfReported, and the call expands to nothing
A function parameter that is not a nameReported, and the parameter is never replaced

Going further