Recursion in CSS:

The Corner Hit Timer Experiment

  • ⚠️ Math Warning

    There's a fair amount of math in this article. I'll explain it as clearly as I'm able.

  • ⚠️ Browser Support Warning

    The live demo only works on version Chrome 125 or newer. Visit chrome://version to check your current version.

  • ⚠️ Sequel Warning

    Having read my CSS-only DVD Screensaver article is recommended to fully understand the context of this article.

  • ⚠️ Performance Warning

    Client-side performance for the demo isn't the best, but should work fine on faster devices.

You're still here? A brave one, I see. Let's get going.

As a fun extra for my DVD screensaver article, I built a corner hit calculator that applies a mathematical formula to calculate how often the bouncing logo hits a corner of the screen.

However, I didn't want to stop at the formula, I wanted a working CSS-only corner hit timer.

I played with a few ideas, but browser support simply wasn't there yet. My solution dropped too many frames due to performance issues.

That's until Chrome 125 came along with support for the CSS mod function, and now we're here. See the timer live on the next slide!

Sorry, the interactive experience is not available on smaller screens.

How did I build this timer?

Essentially, there's two problems I had to solve:

  1. Building a countdown: There's an excellent article at CSS tricks that explains how to leverage @property animations and counter-reset to build a timer.
  2. Calculating the corner hit cycle time: this is definitely the main challenge, as I wanted to do it without any JavaScript. Let's get to it.

From the article I linked earlier, the formula for the corner hit cycle time is lcm(a, b) / speed, where a is the screen width minus the logo width and b is the screen height minus the logo height.

lcm(a, b) (Lowest Common Multiple of a and b) can be expressed as a * b / gcd(a, b), where gcd stands for Greatest Common Divisor.

We've distilled our corner hit cycle problem into the more generic GCD problem. However, calculating the greatest common divisor of two integers is a complex problem, usually solved through the Euclidean Algorithm.

Can we implement the Euclidean Algorithm with CSS?

The algorithm itself is quite simple, and there's both iterative and recursive implementations of it.

Pseudocode for the recursive version looks like this:

a ≥ b
function gcd(a, b)
  if b ≠ 0
    return gcd(b, a mod b)
  else
    return a

There's no such thing as arbitrary recursion in CSS, since you can't even define functions. Given two arbitrary integers, getting the GCD is not feasible. However, if we look a little further, the numbers in our use case are not entirely aribtrary.

We can take advantage of a mathematical proof that states that the number of steps for the euclidean algorithm can never be more than five times the number of digits in the smaller number b.

If there's an upper bound to the screen dimensions (and there is), we know the maximum number of steps we might need.

Why does that help us?

Well, there's no arbitrary recursion in CSS, but nothing stops me from nesting a bunch of divs like this:

.step
.step
.step
.step
.step

If we made every .step element calculate one step of the algorithm over two CSS variables --a and --b, we could be onto something.

.step {
  --a: var(--b);
  --b: mod(var(--a), var(--b));
}

I was really disappointed when that didn't work. Turns out we can't set --a in an element that also uses its value.

No problem, just add more divs:

.step
.rename-variables
.step
.rename-variables
.step

We calculate the algorithm step on two new variables, then on the following element we assign the original ones back.

.step {
  --a2: var(--b);
  --b2: mod(var(--a), var(--b));
}

.rename-variables {
  --a: var(--a2);
  --b: var(--b2);
}

That's nice and all, but it still doesn't work.

The number of steps, and thus the amount of nested elements, has a known ceiling but isn't constant. Our b ≠ 0 check is still missing, the algorithm runs mod over and over again with no regard for the condition.

This casues the result to be wrong because if we don't stop at the right moment, on the next step a mod 0 equals 0 and the result will be zero on all steps after that, too.

How do we stop the recursion without access to an if statement? There are no conditional structures in CSS, right?

That's not exactly true: if you've ever styled a responsive application, you've used media queries, which allow us to conditionally apply some styles.

Hold on, we want to conditionally apply the step if the previous step's b value is not 0, and media queries query the viewport, not the parent element...

We can use iframes container queries, of course.

Container queries (also a new browser feature) allow us to define an element as a container, then any children rules who use the @container directive will query the nearest container ancestor.

@container (min-width: 1px) {
  .step {
    --a2: var(--b);
    --b2: mod(var(--a), var(--b));
    width: calc(var(--b2) * 1px);
    container-type: inline-size;
}

.rename-variables {
  --a: var(--a2);
  --b: var(--b2);
}

Each .step element defines itself as a container and sets its own width to the value of b. Visually, this is irrelevant, since it's a fully transparent element.

However, the following step's rule will only run if the container width is 1px or larger, meaning it will stop the execution when it needs to.

Conclusion

Just because you can, it doesn't mean you should.

This was only an experiment to push the boundaries of what can be programmed with CSS. I felt dirty after writing the words "No problem, just add more divs". Don't quote me on that.

I can't think of any practical uses for this, but I'd love to hear about them if you find any.

I hope you enjoyed the article and thought "oh... that's creative" at least once while going through it. Thanks for reading!


Published on June 13, 2024.