An Introduction to css-doodle

9 min read Original article ↗

This is a rough transcript based on my notes for the sharing session I presented at the online Groove meetup in June. I've shared the slides before, but I think it's worth putting the content in writing. The full slide deck is accessible on GitHub.

css-doodle is a project I started many years ago. Initially, it was used to draw some simple graphic patterns, but over time, it evolved into a tool for creative coding and generative art. I’ve always wanted to write an article about css-doodle, and today I finally have the chance to give an introduction to it.

slide cover

So how did css-doodle begin?

One day I came across a background pattern on Dribbble, by the product designer Jan-Paul Koudstaal. I was attracted by its colors and simplicity, and thought it might be a fun exercise to recreate it with CSS.

I’m sure many people have had the same experience as me -- seeing a graphic shape and wondering if it could be created with CSS.

slide 3

The first solution came to me was to use a grid of elements with different background colors and border radius values. The problem is that manually creating and assigning values to each element can be quite tedious. Although there are preprocessors helping me to generate HTML, there’s still a lot of code.

In CSS, there are usually multiple ways to do just one thing. For example, the quarter circle in this pattern could also be made with clip-path or radial-gradient. What I want is to focus on these key properties.

slide 4

To explore CSS properties more efficiently, I used JavaScript to generate the grid and setup all the random variables. This is an earlier Web Component project prior to css-doodle that did the code generation.

slide 5

The idea behind css-doodle originated from that earlier project. The main difference is that it uses CSS directly rather than JavaScript to describe the styles. This is how the css-doodle code looks like to create the same pattern:

First, we generate a 6x5 grid with the container width in 37vw and a faint background color. Next, for each cell I pick a random color from the given color list using the @p() function. Then the same way to randomly pick border-radius values from another list generated with the @cycle() function.

slide 6

Another way to create this pattern is to use clip-path and the native circle() function.

slide 7

Or use radial-gradient.

Create the circle shape using radial-graident and then place the circle to 4 different places.

As you can see the syntax of css-doodle is very similar to CSS, with the addition of a few extra functions.

slide 8

css-doodle doesn’t have a CSS parser like SASS or any other pre-processors. It doesn’t interpret the syntax of colors or gradients but takes a different approach.

One reason for this is that I can’t catch up the evolution of CSS on my own. New features, such as units and color functions, are being added to CSS all the time. It would take time to sync the updates.

This turned out to be the right decision, as I can always use the latest CSS features without upgrading css-doodle.

slide 9

A Typical CSS rule consists of three different parts: Selector, Property, and Value.

slide 10

css-doodle aims to extend CSS in these three parts. It doesn’t alter anything, only including several new selectors and properties, which begin with the @ symbol in order to distinguish them from native properties and selectors.

slide 11

There is an example -- a grid where @i represents the index value of the current cell. setting the value to @content inserts the value as a text node, I often use it for debugging.

The @odd selector applies the rules in its block only to cells with odd indices, so only the odd-numbered cells have a black background.

You can also set a background color for a specific cell, the CSS cascade rules will apply as expected.

slide 12

The variables attached to the cells are quite useful for making differentiated changes. In this case, I give each cell a different size and rotation according to its index value.

slide 13

The @i is a variable as well as a function. Instead of using calc(), I can also put expression in function parameter to get the calculation with @i directly.

slide 14

Let’s get into the function syntax.

slide 15

If there’s no argument or parameter for the function, the parentheses can be omitted. Here the first and the second are literally the same.

If there’s an operator attached at the head or the tail of the parameter it will try to do calculations with itself and the returned function value.

slide 16

This is another function where the previous three are equal.

If the first parameter is a number, the function name and the number can be put together to form another function, It’s like curry in functional programming.

Multiple functions can be chained together using dots, known as function composition. The output of each function will be the input of the previous one.

slide 17

Here is an example of the random distribution by calling the @r function four times.

slide 18

Random functions play an important role in generative art. In css-doodle there are two categories of random functions.

slide 19
slide 20

There are five cells, each will pick a value from 1 to 5. The pick functions also recognize a shorthand input like regular expressions, to save some characters. The upper case @P will pick a random value but ensure the next one is different from the previous value. It can be useful in some scenarios like picking colors.

slide 21

css-doodle can be used to as a tool for learning CSS properties. It's easy to setup a grid and make a comparison to different values.

slide 22

Another group is random value generation, @r and @R (or @rn).

The lower case @r is to get a random value from a given range, defaults to 0 to 1.

The uppercase @R is to get a random value using Perlin noise.

slide 23

In P5.js you need to use noise() and map function together, while in css-doodle the @R() function will do the two actions in one place. After getting the noise value it will map the value to a given range according to the context.

slide 24

There's an example using the noise function @R().

slide 25

By default css-doodle will use the current timestamp for random seed. If you forget to set a random seed and also want to keep the current result, you can always use devtools or JavaScript to get the seed property from the css-doodle element.

slide 26

There are some other kind of functions for generating. I call them generator functions.

slide 27

These functions generate values or transform the input to other forms, such as repeating or reversing a list. They are usually used in conjunction the picking group functions like @p().

slide 28

The most frequently used is function @m(). It accepts two parameters, the first is the iteration count, the second is the value to be generated.

In CSS, properties like background-imagebox-shadow accept multiple values, and these values are separated by commas. So the @m() function will join the values with commas automatically.

slide 28

The @m() function is much like a for-loop but in a declarative way.

Sometimes I don't wish CSS to adopt for-loops just to mirror other programming languages. There are certainly different ways to achieve the same result, like @m(). We need to explore more possibilities in declarative syntax.

slide 30

OK next, background functions.

slide 31

I have experimented with many functions that generate images to CSS background. The CSS painting API also uses background. It seems CSS background is a desirable place to extend the ability of CSS.

slide 32

To me CSS background is like a mirror or a digital screen, everything inside it is virtual and untouchable since there's no actual DOM inside there. But it gives us a window for imagination and a bridge to connect other things.

slide 33

With @doodle function I can use the rule of css-doodle to generate a background image. CSS itself can not generate new elements but I can open another dimension inside background. No gradient hacks anymore.

slide 34

They can even be nested.

slide 35

I added a @shaders() function to enable shader programming more straightforward. I haven’t used it much. The shader function and the removed canvas() function is quite low lever, which means one needs to write a lot of code to draw something.

slide 36

I prefer to use the @pattern() function. It was built on top of shaders and the syntax was designed similar to CSS. It’s much higher lever and has a single purpose. Maybe I should build more functions like this.

slide 37

Browsers support SVG as CSS background natively, so I simplified the syntax of SVG to make it more like writing CSS, encouraging me to use SVG more.

slide 38

If you're interested in this idea, there’s a detailed blog post about this syntax:

slide 39

The last section is about shapes. I'll skim through it.

slide 40

These are the builtin shapes in css-doodle, which are generated with clip-path.

slide 41

To make one element into a shape, you can use the @shape property declaratively. Another option is to use the @shape() function.

slide 42

The @shape() function is also a generator, it provides several commands to define a customized shape with mathematical functions.

slide 43

Simple rules can create a wide variety of patterns. It’s fascinating to discover some new shapes this way.

slide 44
slide 45

This is one of my favorite shapes, which demonstrates the power of negative space.

slide 46

This video mentioned about the negative space and talked about the importance of setting limits, which reminded me of css-doodle, particularly its performance considerations and the limitations of CSS grids.

Creating visually appealing graphic designs or artwork doesn’t require a lot of elements. The key is to view things differently. I highly recommend watching this video. It’s truely beautiful.

slide 47

Here is the project website and a collection of CodePen demos using css-doodle. The documentation is not complete, and I still need to work on it.

slide 48

I hope you enjoy this introduction to css-doodle. Thank you!

slide 49