Tomasz Wegrzanowski
Posted on November 23, 2021
I had the first two languages planned before I started the series. For the third I decided to ask GitHub Copilot. Its suggestions were:
- write a lot more episodes about Python
- go alphabetically from C to Rust, then continue about Rust for the rest of the series
- actually write a lot of languages but with a lot of repetition
- HTML
OK, so maybe AI won't be replacing us anytime soon. But that last suggestion wasn't too crazy - HTML may not be a programming language, but CSS basically turned into one!
This episode is not about centering elements, or any such things, we'll be writing real programs in CSS!
Hello, World!
First, the Hello World!
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<style>
:root {
--who: "World";
}
body {
margin: 0;
min-height: 100vh;
font-size: 48px;
display: flex;
justify-content: center;
align-items: center;
}
body::after {
content: "Hello, " var(--who);
}
</style>
</head>
<body>
</body>
</html>
Which looks just as you expected, without any content in the HTML body:
Ignore rules on the body
, they're just centering. The interesting techniques are elsewhere.
We can set variables in CSS with --variablename: value;
. It is then inherited by every child element. We can use such variables with var(--variablename)
.
And we can create "pseudo-elements" like ::after
and ::before
, and set their content. These were added to CSS to deal with things like list numbering. When you say <ol><li>One</li><li>Two</li></ol>
, HTML actually needs to display 1. One 2. Two
. Where those "1." and "2." come from? From just such pseudo-elements (in this case ::marker
, but close enough).
Most "programming with CSS" will rely heavily on pseudoelements. Oh and they're also confusingly named - body::after
means "inside body; after all contents" not "after body".
FizzBuzz
Let's write the real program now, the FizzBuzz! For this we'll put 100 empty spans in the HTML, and do the FizzBuzz with pure CSS:
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
min-height: 100vh;
counter-reset: fizzbuzz-counter;
}
span {
counter-increment: fizzbuzz-counter;
}
span::after {
content: ", ";
}
span:last-child::after {
content: ".";
}
span::before {
content: counter(fizzbuzz-counter);
}
span:nth-child(3n)::before {
content: "Fizz";
}
span:nth-child(5n)::before {
content: "Buzz";
}
span:nth-child(15n)::before {
content: "FizzBuzz";
}
</style>
</head>
<body>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</body>
</html>
And what's that? A real nicely formatted FizzBuzz:
How does it work? We use a few new techniques here.
Every span has two pseudoelements, ::before
and ::after
.
The ::before
pseudoelement gets the counter value or "Fizz" or "Buzz" or "FizzBuzz". To support such important features as light and dark stripes on tables, CSS lets us apply rule to elements every N elements. Rules of same specificity written later take precedence. So span:nth-child(5n)::before
will only apply to every 5th element, except those for which span:nth-child(15n)::before
takes precedence.
We don't use CSS variables for this, we use CSS counters. Counters are created with counter-reset: countername;
, incremented with counter-increment: countername;
, and then accessed with counter(countername)
.
The ::after
pseudoelement is just either comma for all other elements, or period for the final element which we select with :last-child
. This feature is actually occasionally used in real life, to represent lists as sentences.
Counters are also more useful than you'd think - you don't need them for lists, but for things like header numbering with section and subsection numbers, CSS can do that quite easily with counters.
Fibonacci Numbers
And now we run into a very unexpected problem:
- CSS has strings and numbers AND NO WAY TO CONVERT ONE TO THE OTHER!
- all calculations can only be done on numbers
- all
content
display must be strings -
counter(...)
returns a string -
counter
can only be set to a constant integer, or incremented by a constant integer, not to a calculated one
Totally baffling. I've never seen a languge in my life which didn't have a way to print numbers, but that's how we got here.
Well, let's disregard all that and just make a series of bars of Fibonacci numbers size.
We'll need to do calculations with CSS variables not CSS numbers. CSS variables have access to their parent's variables, not their siblings, so we'll need to do some deep nesting. Also unfortunately while we can do some calculations on them, CSS properties aren't really ordered, so we cannot do multiple mutually dependent changes on one layer. So we'll use 3 nested spans per Fibonacci number. And as CSS doesn't have any global :nth-element-globally(3n)
, we'll give them specific classes.
It's possible that these limitations could be avoided, and we'll likely get CSS features that will let us code it in a nicer way (most likely number to string conversion).
Even with these limitations, I think it's still a neat result.
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<style>
:root {
--x: 0px;
--y: 1px;
}
body {
margin: 8px;
font-size: 48px;
min-height: 100vh;
}
span {
display: block;
}
span.a {
--z: calc(var(--x) + var(--y));
}
span.b {
--x: var(--y);
}
span.c {
--y: var(--z);
}
span.c::before {
display: block;
background-color: #480;
height: 5px;
width: var(--x);
margin: 2px;
content: "";
}
</style>
</head>
<body>
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
<span class="a"><span class="b"><span class="c">
</body>
</html>
Which looks like this:
There is definitely more
While this was a quick introduction to programming with CSS, this is by no means its limits.
People have been coding whole CSS-only games like this one or this one.
Will learning that make you a better frontend developer? Not really. But it is definitely fun!
Code
All code examples for the series will be in this repository.
Posted on November 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.