natechoe.dev The blog Contact info Other links The github repo

CSS Puzzle: Animated frames

I made this neat demo with no javascript, check it out!

This is a really interesting puzzle: how can you animate text like this while preserving nested elements such as <span>s with only a single keyframe so that things don't get out of sync?

It's pretty easy to get one of these conditions. You can preserve nested elements with multiple keyframes by just having a few separate elements that synchronously flash, like this:

<!DOCTYPE html>
<html>
<head>
<style>
@keyframes m1 {
  0% { visibility: visible; }
  50% { visibility: visible; }
  50.001% { visibility: hidden; }
  100% { visibility: hidden; }
}
@keyframes m2 {
  0% { visibility: hidden; }
  50% { visibility: hidden; }
  50.001% { visibility: visible; }
  100% { visibility: visible; }
}
#m1 {
  animation: 2s m1 infinite;
}
#m2 {
  animation: 2s m2 infinite;
}
</style>
</head>
<body>
<p id=m1><span style='color: #f00'>Hello world!</span></p>
<p id=m2><span style='color: #00f'>Goodbye world!</span></p>
</body>
</html>

Obviously these elements are offset, it's certainly possible to get this working properly, probably with some position: absolute shenanigans, but that would make the code a bit more complicated, and I don't feel like writing that right now.

This does work, but if these two keyframes ever go out of sync, it could cause some visual artifacts. We could try using the before and after pseudo elements to combine everything into a single keyframe, like this:

<!DOCTYPE html>
<html>
<head>
<style>
@keyframes m {
  0% { content: "Hello world!" }
  50% { content: "Hello world!" }
  50.001% { content: "Goodbye world!" }
  100% { content: "Goodbye world!" }
}
#m::before {
  animation: 2s m infinite;
  content: "";
}
</style>
</head>
<body>
<p id=m></p>
</body>
</html>

Note: This code, and to a certain extent this entire article, was inspired by this truly awful clock that I saw on Reddit.

This is always in sync, but since our content is in a replaced element, we can't add spans to change colors.

It would be really nice if every element could reference a single clock, like this pseudo-code:

define clock

forever {
  clock = 0
  sleep 1
  clock = 1
  sleep 1
}

element 0 {
  if clock == 0
    display: block
  else
    display: none
}

element 1 {
  if clock == 1
    display: block
  else
    display: none
}

We do sort of have if statements in CSS with the newly-introduced container queries. We can set the properties of some element based on the width of some parent element. We could then use the width of an element as a substitute for the clock, and just put all of our elements into a div with position: absolute so that the "width" of the grandparent element doesn't matter. Here's the final code that I wrote:

<!DOCTYPE html>
<html>
<head>
<style>
@keyframes snap {
  0% { width: 1px; }
  20% { width: 2px; }
  40% { width: 3px; }
  60% { width: 4px; }
  80% { width: 5px; }
  100% { width: 6px; }
}

#realbody {
  position: absolute;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vw;
}

.msg {
  animation: 2s snap infinite;
  container-type: inline-size;
}

.hide {
  display: none;
}

@container (min-width: 1px) {
  #h0 {
    display: block;
  }
}
@container (min-width: 2px) {
  #h0 {
    display: none;
  }
  #h1 {
    display: block;
  }
}
@container (min-width: 3px) {
  #h1 {
    display: none;
  }
  #h2 {
    display: block;
  }
}
@container (min-width: 4px) {
  #h2 {
    display: none;
  }
  #h3 {
    display: block;
  }
}
@container (min-width: 5px) {
  #h3 {
    display: none;
  }
  #h4 {
    display: block;
  }
}
</style>
</head>
<body>
<div class=msg>
<div id=realbody>
<p class=hide id=h0>How can I do <span style='color:#f00'>this</span> 0?</p>
<p class=hide id=h1>How can I do <span style='color:#f0f'>this</span> 1?</p>
<p class=hide id=h2>How can I do <span style='color:#0f0'>this</span> 2?</p>
<p class=hide id=h3>How can I do <span style='color:#0ff'>this</span> 3?</p>
<p class=hide id=h4>How can I do <span style='color:#088'>this</span> 4?</p>
</div>
</div>
</pre>
</body>
</html>