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.
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.
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.
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.
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.
Another way to create this pattern is to use clip-path
and the native circle() function.
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.
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.
A Typical CSS rule consists of three different parts: Selector, Property, and Value.
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.
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.
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.
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.
Let’s get into the function syntax.
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.
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.
Here is an example of the random distribution by calling the @r function four times.
Random functions play an important role in generative art. In css-doodle there are two categories of random functions.
The first group is picking, to pick a value from a given list. The name is inspired by the Logo programming language.
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.
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.
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.
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.
There's an example using the noise function @R().
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.
There are some other kind of functions for generating. I call them generator functions.
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().
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-image, box-shadow accept multiple values,
and these values are separated by commas.
So the @m() function will join the values with commas automatically.
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.
OK next, background functions.
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.
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.
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.
They can even be nested.
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.
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.
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.
If you're interested in this idea, there’s a detailed blog post about this syntax:
The last section is about shapes. I'll skim through it.
These are the builtin shapes in css-doodle, which are generated with clip-path.
To make one element into a shape, you can use the @shape property declaratively.
Another option is to use the @shape() function.
The @shape() function is also a generator,
it provides several commands to define a customized shape with mathematical functions.
Simple rules can create a wide variety of patterns. It’s fascinating to discover some new shapes this way.
There’s the playground and a blog post exploring more shapes.
This is one of my favorite shapes, which demonstrates the power of negative space.
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.
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.
I hope you enjoy this introduction to css-doodle. Thank you!