Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Tue, 04 Nov 2025 00:46:28 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Numbers That Fall (Scroll-Driven Animations & Sibling Index) https://frontendmasters.com/blog/numbers-that-fall-scroll-driven-animations-sibling-index/ https://frontendmasters.com/blog/numbers-that-fall-scroll-driven-animations-sibling-index/#comments Tue, 07 Oct 2025 19:40:28 +0000 https://frontendmasters.com/blog/?p=7338 Maybe you noticed the big number at the bottom of the current design of this site? As I write, that number is $839,000 and it’s just a reminder of how Frontend Masters gives back to the open source community. I did that design so I figured I’d write down how it works here.

Figuring out the Scroll-Driven Animation

It’s helpful to just stop and think about how you want the animation to behave in regard to scrolling. In this case, I’d like the numbers do be done animating shortly after they arrive on the page from the bottom. This means they will be visible/readable most of the time, which feels appropriate for text especially.

With this in mind, I recommend heading to Bramus Van Damme’s tool View Progress Timeline: Ranges and Animation Progress Visualizer. This tool is extremely useful to play around with to understand the different possibilities with different sizes of content. After various playing, I found a good match of animation-range-start and animation-range-end values for what I had in mind.

In the video above, we’re seeing a “fly in” animation. But that animation doesn’t matter. It’s just showing us the time range that will be relevant to whatever animation we choose to apply. Our numbers are going to “fall in”, and we’ll get to that soon.

Split the Numbers Up

We’ll put each number in a <span> so we can animate them individually. But we’ll make the accessible text for that component read properly with an aria-label attribute:

<div class="dollar-amount" aria-label="$839,000">
  <span class="dollar-sign">$</span>
  <span>8</span>
  <span>3</span>
  <span>9</span>
  <span>,</span>
  <span>0</span>
  <span>0</span>
  <span>0</span>
</div>

Animate Each Span

Each one of the numbers will have the same animation:

.dollar-amount {
  ...

  > span {
    display: inline-block;
    animation: scroll-in linear both;
    animation-timeline: view();
    animation-range: cover 0% entry-crossing 120%;
  }
}

The animation we’ve named there scroll-in might look like this:

@keyframes scroll-in {
  from {
    scale: 1.33;
    opacity: 0;
    translate: 0 -50px
  }
}

That will make each letter “fall” from 50px above down to it’s natural position, while also fading in and starting bigger and ending up it’s intrinsic size.

But, they will all do that the exact same way. We want staggering!

Staggering in a Scroll-Driven World

Usually animation staggering uses transition-delay or animation-delay on individual “nth” elements. Something like:

.dollar-amount {
  span:nth-child(1) { animation-delay: 0.1s; }
  span:nth-child(2) { animation-delay: 0.2s; }
  span:nth-child(3) { animation-delay: 0.3s; }
  ...
}

But that’s not going to work for us here. Delay in a scroll-driven animation doesn’t mean anything. I don’t think anyway?! I tried the above and it didn’t do anything.

Fortunately, the effect I was going for was a bit different anyway. I wanted the numbers to have a staggered fall in effect (see video above) where subsequent numbers almost look like they are falling from a different height and yet all arrive at the same time. So I could handle that like…

.dollar-amount {
  span:nth-child(1) { translate: 0 -20px; }
  span:nth-child(2) { translate: 0 -40px; }
  span:nth-child(3) { translate: 0 -60px; }
  ...
}

But, if we’re being really future-looking, we can handle it 1) within the @keyframes 2) in one line.

@keyframes scroll-in {
  from {
    scale: 1.33;
    opacity: 0;
    translate: 
      /* x */ calc(sibling-index() * 4px)
      /* y */ calc(sibling-index() * -20px);
  }
}

The sibling-index() function is perfect for staggering of any kind. It’ll return 1 for what would be :nth-child(1), 2 for what would be :nth-child(2), etc. Then we can use that integer in a calculation or delay.

Demo

That should do it!

(Note this will only work in Chrome’n’friends due to the sibling-index() usage. I’ll leave it as an exercise for the reader to write a fallback that supports a deeper set of browsers.)

The part that feels the weirdest to me are the “magic number”y values as part of the animation-range. But I guess they are about as magic as font-size or the like. They are values that describe the animation that works best for you, even if they are a little hard to immediately visualize.

]]>
https://frontendmasters.com/blog/numbers-that-fall-scroll-driven-animations-sibling-index/feed/ 2 7338
Scroll-Driven Letter Grid https://frontendmasters.com/blog/scroll-driven-letter-grid/ https://frontendmasters.com/blog/scroll-driven-letter-grid/#comments Mon, 09 Jun 2025 22:28:59 +0000 https://frontendmasters.com/blog/?p=6059 I was thinking about variable fonts the other day, and how many of them that deal with a variable axis for their weight go from 100 to 900. It varies — so you can always check wakamaifondue.com if you have the font file. Jost on Google Fonts is a classic example. Load that sucker up and you can use whatever weight you want.

I was also thinking about the “simple” kind of scroll-driven animations where all it does is move a @keyframe animation from 0% to 100% while a scrolling element goes from 0% to 100% “scrolled”. Fair warning that browser support isn’t great, but it’s just a fun thing that can easily just not happen.

It’s deliciously simple to use:

We can smash these things together. We should be able to map 0%-100% to 100-900 pretty easily, right?

Right.

Let’s made a grid of 100 letters inside a <div id="grid">. We could use any kind of HTML generating technology. Let’s just vanilla JavaScript here.

function generateGrid() {
  const grid = document.getElementById("grid");
  grid.innerHTML = "";

  for (let i = 0; i < 100; i++) {
    const div = document.createElement("div");
    div.textContent = getRandomLetter();
    grid.appendChild(div);
  }
}

generateGrid();

The lay it out as a 10✕10:

#grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
}

We can chew through that grid in Sass applying random weights:

@for $i from 1 through 100 {
  #grid :nth-child(#{$i}) {
    font-weight: 100 + math.ceil(random() * 800);
  }
}

Looks like this.

But scroll up and down that preview!

I attached a scroll timeline to the document like:

html {
  scroll-timeline: --page-scroll block;
}

Then use that timeline to call an animation like:

#grid {
  > div {
    animation: to-thin auto linear;
    animation-timeline: --page-scroll;
  }
}

That animation is named to-thin, but actually I made three different animations: to-thick, to-thin, and to-mid, then applied them in rotation to all the letters, so any given letter does something a bit different.

@keyframes to-thick {
  50% {
    font-weight: 900;
  }
}
@keyframes to-thin {
  50% {
    font-weight: 100;
  }
}
@keyframes to-mid {
  50% {
    font-weight: 450;
  }
}

See how I used 50% keyframes there which is a nice trick to animate to that value half way through the animation, then back.

It then occurred to me I could make a secret message. So I make a @mixin that would override certain letters in CSS to make the message. It still randomized the weight, but all the letters animate to thin while the secret message animates to thick, revealing it as you scroll down.

Anyway this is sometimes how I spend my free time and it’s completely normal.

]]>
https://frontendmasters.com/blog/scroll-driven-letter-grid/feed/ 1 6059
Scroll-Driven & Fixed https://frontendmasters.com/blog/scroll-driven-fixed/ https://frontendmasters.com/blog/scroll-driven-fixed/#respond Fri, 20 Dec 2024 17:00:57 +0000 https://frontendmasters.com/blog/?p=4812 Scroll-driven animations is a good name. They are… animations… that are… scroll-driven. As you scroll you can make something happen. The most basic kind, where a @keyframe is ran 0% to 100% as the element is scrolled 0% to 100% is particularly easy to wrap your mind around.

I also think it’s fun to mess with the expectations of scrolling.

In very light, fun-only, non-crucial ways. Not in ways that would hurt the access of content.

Like what if we made an element that definitely scrolled:

.scrolling-element {
  height: 100dvh;
  /* ... make something inside it taller than it is ... */
}

But then all the content within it was position: fixed; so it didn’t move normally when the element was scrolled.

.scrolling-element {
  height: 100dvh;
  > * {
    position: fixed;
  }
}

Instead, we could have the elements react the scroll position however we wanted.

.scrolling-element {
  scroll-timeline-name: --my-scroller;
  scroll-timeline-axis: block;
  
  > * {
    position: fixed;
    animation: doSomethingCool linear;
    animation-timeline: --my-scroller;
  }
}

@keyframes doSomethingCool {
  100% {
    rotate: 2turn;
  }
}  

Here’s that basic setup:

I bet you could imagine that this is the same exact trick for a “scroll position indicator” bit of UI. Position that <div> as like a 2px tall bar and have the scaleX transform go from 0 to 100% and donezo.

I’ll use the same spirit here to have a whole grid of cells use that “scale to zero” animation to reveal a hidden picture.

I think that hidden picture thing is fun! I’m imagining a game where you have to guess the picture by scrolling down as little as possible. Like “name that tune” only “name that movie still” or whatever.

In this next one I took the idea a bit further and create randomized positions for each of the grid cells to “fly off” to (in SCSS).

I find that extraordinary that that kind of interaction can be done in HTML and CSS these days.

]]>
https://frontendmasters.com/blog/scroll-driven-fixed/feed/ 0 4812
(Up-) Scoped Scroll Timelines https://frontendmasters.com/blog/scoped-scroll-timelines/ https://frontendmasters.com/blog/scoped-scroll-timelines/#comments Mon, 11 Nov 2024 17:36:23 +0000 https://frontendmasters.com/blog/?p=4365 I keep learning little details about scroll-driven animations!

I started this little journey thinking about if you wanted to do special styling when a page scrolled through a certain section. I thought then that in order to pass scrolling information to descendants, you’d have to do it with --custom-properties. That’s sometimes still a decent idea, but it’s not strictly true, as those descendants can inherit a named timeline and tap into that to do styling on themselves.

Then I thought, while that’s a nice improvement, it’s still limited in the sense that only descendants can tap into a higher-up-the-DOM element’s timeline. Like an enforced parent/child situation. Turns out this isn’t true either, and again thanks to Bramus for showing me how it works.

Since we’re three-deep here on this journey, I figure calling it a series makes sense:

Article Series

Fair warning all this stuff is Chrome ‘n’ friends only right now. But I’ve seen flags in both Safari and Firefox so it’s coming along.

You can have any element on the page “listen” to a scroll (or view) timeline of a totally different element.

That’s the rub.

I wrongly assumed it had to be a parent/child thing (or parent/descendant). By default, that’s true, but if you intentionally move the scope of the timeline to another element up the DOM, you can make it work for any elements.

I’ll illustrate:

Demo of that:

The idea above is that you scroll the element on the top and the element below rotates. They are sibling elements though, so this is only possibly by “hoisting” the scroll-timeline to a higher-in-the-DOM element with timeline-scope so that the other element can pick up on it.

My ridiculous head thought of trying to make a quiz game or some kind of unlocking puzzle with getting scroll positions just right. I proved out the idea here:

There are a bunch of abused CSS tricks in there:

  1. Declare a custom property with @property so it’s value can be animated
  2. Make a scrolling element with a scroll-timeline
  3. Hoist that timeline up to a parent element
  4. Have the “number” element explicitly use that timeline
  5. Make the @keyframes animate that <integer> custom property
  6. Display the number using a pseudo element and counter()
  7. Use @container style() queries to check when the custom property is exactly the “answer” and update styling.

Phew. It all kinda leads up to that very last step where we can react to a value that came from a user scrolling. It might be a fun little project to build a bike lock number-twister thing with this.

Article Series

]]>
https://frontendmasters.com/blog/scoped-scroll-timelines/feed/ 3 4365
Named Scroll & View Timelines https://frontendmasters.com/blog/named-scroll-view-timelines/ https://frontendmasters.com/blog/named-scroll-view-timelines/#comments Mon, 04 Nov 2024 16:22:35 +0000 https://frontendmasters.com/blog/?p=4327 I just blogged about a way to pass information from an element that had a view timeline on itself down to it’s descendent elements. The idea is to set up CSS custom properties with @property and the @keyframe would animate those, thus the descendent’s would have access to them.

It worked fine (noting that scroll-driven animations are, so far, only in Chrome).

But Bramus noted that another possibility there was to name the view timeline and have the children reference that instead. His video on this is informative.

Article Series

The update to my original demo was pretty easy to do. Remember it had a section with a “pull quote”. The section had the view timeline, the pull quote is what I was trying to animate. So here I name the timeline (and note the scroll direction: block means vertical in logical property world):

.has-pullquote {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 100%;
  view-timeline: --section-pullquote block;
}

You have to name the timeline with a --double-sash name like that, which in modern CSS parlance is referred to as custom ident. It looks like a custom property but it isn’t, it’s just a unique name.

Now that the timeline is named, any descendent can reference it for it’s own animation timeline. Again specific to my demo, I switched things up to work like this (just the basics):

blockquote {
  /* Pull quote styling... */ 
  
  animation: blockquote linear both;
  animation-timeline: --section-pullquote;
}

@keyframes blockquote {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

Here’s an updated demo using that technique instead of animating all custom properties.

The actual demo updates a few more things just to have a bit more of a play and try things out. Notably, one of the effects still required me to animate a custom property still. That’s because I’m animating the color-stop of a gradient, and since that’s just a part of a whole valid value, it really needs to be done as a custom property.

Still, most of the animation work was moved over to using a keyframe applied directly to the descendent element itself, which I think is a logical improvement and I think it’s very cool how you can do that.

Essentially: you can name a view (or scroll) animation timeline (view-timeline or scroll-timeline) and any descendent element can tap into it and base it’s animation off of that (animation-timeline).

The fact that it works for descendents only is interesting to me. When I was playing with this to try to understand it, my first thought was to try to make a random element on the page have a scroll-timeline and another totally random element on the page “listen” to that one for it’s animation-timeline. I made them siblings and was confused why it wasn’t working. That ain’t gonna work, apparently, gotta have that parent/child thing going on. (Bramus’ video notes another gotcha: intermediary parents with overflow: hidden ruining the effect, and a perfect solution: overflow: clip).

I updated my playing to make it a parent/child relationship, and here’s that silly idea:

There you can see the “number” using the “scroller”s scroll-timeline for it’s animation-timeline and “animating” an integer value. If I wanted to take that integer and place it wherever on the page, it’s somewhat limiting because the parent container necessarily has overflow on it so it scrolls.

It does make me wonder if anchor positioning or even some abuse of popover would be able to pop it out of that constrained container, but that’ll have to be an exploration for another day.

Article Series

]]>
https://frontendmasters.com/blog/named-scroll-view-timelines/feed/ 1 4327
Edge to Edge Text https://frontendmasters.com/blog/edge-to-edge-text/ https://frontendmasters.com/blog/edge-to-edge-text/#comments Thu, 31 Oct 2024 22:33:58 +0000 https://frontendmasters.com/blog/?p=4294 I kid you not: Roman Komarov’s Fit-to-Width Text is one of my favorite CSS tricks I’ve ever seen. It’s, uh, quite a treat (that’s all you’re going to get here on Halloween, sorry). It’s just so strange. The end result is that you can size a line of text such that it hits the left and right edge of a container perfectly.

This is a very legitimate need that people have been solving for ages. If the container is a fixed size, you can solve it by setting ultra specific font sizes. But more likely these days, containers are of unknown widths, leaving us to JavaScript for figuring out how big of text we can fit in there. FitText was a seminal example. These days, we can also do it with container units, but it’s still extremely fiddly. Wouldn’t it be nice to be like font-size: make-it-fit;?

Roman’s trick is as close to that as can be. Check out his post for all the details, but the core concept is that it uses scroll-driven animations. The text gets set pretty big by default, then a scroll-driven animation is set on it which runs scales the text down essentially until animation-range: entry-crossing; is fulfilled then stops. Here’s an example with just one word (free free to resize and see):

The absolute core of the idea is:

@supports (animation-range: entry-crossing) {
  .fit-to-width {
    font-size: 12rem; /* max-font-size */
    overflow: hidden;

    & > * {
      inline-size: max-content;
      transform-origin: 0 0;
      animation: apply-text-ratio linear;
      animation-timeline: view(inline);
      animation-range: entry-crossing;
      display: block;
    }
  }
}

@keyframes apply-text-ratio {
  from {
    scale: 0;
    margin-block-end: -1lh;
  }
}

Like Roman’s original demo, it works great on multiple lines, actually showing off the power of the technique much better. The design of “multiple lines sized to fit exactly on a line” made me think of those “In this house we believe…” signs, so I made my own:

That demo has contenteditable on it so you can mess with the letters and see it work.

If, like me, you have a hard time wrapping your mind around the trick, note that you can inspect the animations in Chrome DevTools and see how each span has a different length of animation:

I think the longer the animation the more the text scales down toward zero. Phew — I told you it was weird.

]]>
https://frontendmasters.com/blog/edge-to-edge-text/feed/ 3 4294
Scroll-Driven… Sections https://frontendmasters.com/blog/scroll-driven-sections/ https://frontendmasters.com/blog/scroll-driven-sections/#comments Tue, 29 Oct 2024 12:45:59 +0000 https://frontendmasters.com/blog/?p=4277 I was checking out a very cool art-directed article the other day, full of scrollytelling, and, like us web devs will be forever cursed to do, wondering what they used to build it. Spoiler: it’s GSAP and ScrollTrigger.

No shame in those tech choices, they are great. But with scroll-driven animations now being a web standard with growing support, it begs the question whether we could do this with native technologies.

My brain focused on one particular need of the scrollytelling style:

  1. While the page scrolls through a particular section
  2. Have a child element appear in a fixed position and be animated
  3. … but before and after this section is being scrolled through, the element is hidden

Perhaps a diagram can help drive that home:

But I was immediately confused when thinking about how to do this with scroll-driven animations. The problem is that that “section” itself is the thing we need to apply the animation-timeline: view(); to, such that we have the proper moment to react to (“the section is currently in view!“). But in my diagram above, it’s actually a <blockquote> that we need to apply special conditional styling to, not the section. In a @keyframe animation, all we can do is change declarations, we can’t select other elements. Apologies if that confusing, but the root of is that we need to transfer styles from the section to the blockquote without using selectors — and it’s weird.

The good news is that what we can do is update CSS custom properties on the section, and those values will cascade to all the children of the section, and we can use those to style the blockquote.

First, in order to make a custom property animatable, we need to declare it’s type. Let’s do a fade in first, thus we need opacity:

@property --blockquoteOpacity {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

Now the section itself has the animation timeline:

section.has-pullquote {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 100%;
}

And that animation we’ve named reveal above can now update the custom property:

@keyframes reveal {
  from {
    --blockquoteOpacity: 0%;
  }
  to% {
    --blockquoteOpacity: 100%;
  }
}

Now as the animation runs, based on it’s visibility in the viewport, it will update the custom property and thus fade/in out the blockquote:

blockquote {
  opacity: var(--blockquoteOpacity);

  position: sticky;
  top: 50%;
  transform: translateY(-50%);
}

Note I’m using position: sticky in there too, which will keep our blockquote in the middle of the viewport while we’re cruising through that section.

Try it out (Chrome ‘n’ friends have stable browser support):

Here’s a video of it working in case you’re in a non-supporting browser:

Because we instantiated the opacity custom property for the opacity at 100%, even in a non-supporting browser like Safari, the blockquote will be visible and it’s a fine experience.

I found this all a little fiddly, but I’m not even sure I’m doing this “correctly”. Maybe there is a way to tap into another elements view timeline I’m not aware of? If I’m doing it the intended way, I could see this getting pretty cumbersome with lots of elements and lots of different values needing updated. But after all, that’s the job sometimes. This is intricate stuff and we’re using the CSS primitives directly. The control we have is quite fine-grained, and that’s a good thing!

Article Series

]]>
https://frontendmasters.com/blog/scroll-driven-sections/feed/ 9 4277
Custom Range Slider Using Anchor Positioning & Scroll-Driven Animations https://frontendmasters.com/blog/custom-range-slider-using-anchor-positioning-scroll-driven-animations/ https://frontendmasters.com/blog/custom-range-slider-using-anchor-positioning-scroll-driven-animations/#comments Wed, 21 Aug 2024 14:20:29 +0000 https://frontendmasters.com/blog/?p=3569 Anchor positioning and scroll-driven animations are among of the most popular and exciting CSS features of 2024. They unlock a lot of possibilities, and will continue to do so as browser support improves and developers get to know them.

Here is a demo of a custom range slider where I am relying on such features.

This whole UI is a semantic HTML <input type="range">, with another semantic <output> element showing off the current value, along with quite fancy CSS.

Intuitively, you may think there is a JavaScript code somewhere gathering the value of the input “on change” and updating the position/content of the tooltip. As for the motion, it’s probably a kind of JavaScript library that calculates the speed of the mouse movement to apply a rotation and create that traction illusion.

Actually, there is no JavaScript at all.

It’s hard to believe but CSS has evolved in a way that we can achieve such magic without any scripts or library. You will also see that the code is not that complex. It’s a combination of small CSS tricks that we will dissect together so follow along!

At the time of writing, only Chrome (and Edge) have the full support of the features we will be using.

Prerequisites

First, let’s start with the HTML structure:

<label>
  Label
  <input type="range" id="one" min="0" max="120" value="20">
  <output for="one" style="--min: 0;--max: 120"></output>
</label>  

An input element and an output element are all that we need here. The label part is not mandatory for the functionality, but form elements should always be labelled and you need a wrapper element anyway.

I won’t detail the attributes of the input element but note the use of two CSS variables on the output element that should have the same values as the min and max attributes.

In addition to the HTML code, I am going to consider the styling of the range slider and the tooltip as prerequisites as well. I will mainly focus on the new features and skip most of the aesthetic parts, although I have covered some of those aspects in other articles, like here where I detail the styling of the range slider.

As for the tooltip, I have a big collection of 100 different tooltip shapes and I am going to use the #41 and #42. I also have a two-part article detailing the creation of most of the tooltips.

You don’t need the fancy styled tooltip output, nor do you need the custom styling of the range slider itself, it’s just fun and offers some visual control you might want. Here’s a “naked” demo without all that:

The Tooltip Position

The first thing we are going to do is to correctly place the tooltip above (or below) the thumb element of the slider. This will be the job of Anchor positioning and here is the code:

input[type="range" i]::-webkit-slider-thumb {
  anchor-name: --thumb;
}
output {
  position-anchor: --thumb;
  position: absolute;
  position-area: top; /* or bottom */
}

That’s all! No more than four CSS declarations and our tooltip is correctly placed and will follow the movement of the slider thumb.

Anchor positioning is an upgrade of position: absolute here. Instead of positioning the element relatively to an ancestor having position: relative we can consider any element on the page called an “anchor”. To define an anchor we use anchor-name with whatever value you want. It’s mandatory to use the dashed indent notation like with custom properties. That same value can later be used within the absolute element to link it with the “anchor” using position-anchor.

Defining the anchor is not enough, we also need to correctly position the element. For this, we have the position-area.

The position-area CSS property enables an anchor-positioned element to be positioned relative to the edges of its associated anchor element by placing the positioned element on one or more tiles of an implicit 3×3 grid, where the anchoring element is the center cell.

ref

Here is an online tool to visualize the different values.

We’re using position-area: top on the <output>, and a bottom class flips that to position-area: bottom to re-position it and make the design work below.

Here is the demo so far:

Hmmmm, there is an issue! Both tooltips are linked to the same thumb. This is understandable, because I used the same anchor name so the first one will get ignored.

Use a different name, you say, and that’s correct but it’s not the optimal solution. We can still keep the same name and instead, limit the scope using anchor-scope.

label {
  anchor-scope: --thumb;
}

The above code should limit the scope of the anchor --thumb to the label element and its descendant. In other words, the anchor cannot be seen outside the label element.

Another fix is to add position: relative to label. I won’t detail how it works but it has to do with the creation of a containing block.

Hmmmmm. We have fixed the scoping problem but the position of the tooltip is still not good. If you move the thumb to the edges, the tooltip is no longer following. It’s limited to the boundary of the slider. It’s kind of strange, but it’s by design.

By adding position: relative we create a containing block for the tooltip and we trigger the following behavior described by the specification:

If the box overflows its inset-modified containing block, but would still fit within its original containing block, by default it will “shift” to stay within its original containing block, even if that violates its normal alignment. This behavior makes it more likely that positioned boxes remain visible and within their intended bounds, even when their containing block ends up smaller than anticipated.

To fix this, we can use justify-self: unsafe anchor-center;.

When using position-area: top (or bottom), the browser applies a default alignment in the horizontal axis equivalent to justify-self: anchor-center. By adding the unsafe keyword, we allow it to overflow the containing block instead of shifting inside it.

The Tooltip Content

Now that our tooltip is correctly positioned, let’s move to the content. This is where scroll-driven animations enter the story. I know what you are thinking: “We have nothing to scroll, so how are we going to use scroll-driven animations?”

If you read the MDN page you will find something called a “view progress timeline”:

You progress this timeline based on the change in visibility of an element (known as the subject) inside a scroller. The visibility of the subject inside the scroller is tracked as a percentage of progress — by default, the timeline is at 0% when the subject is first visible at one edge of the scroller, and 100% when it reaches the opposite edge.

This is perfect for us since we have a thumb (the subject) that moves inside the input (the scroller) so we don’t really need to have anything else to scroll.

We start by defining the timeline as follows:

input {
  overflow: hidden; /* or `auto` */
}
input[type="range" i]::-webkit-slider-thumb {
  view-timeline: --thumb-view inline;
}

Similar to what we did with the anchor, we give a name and the axis (inline) which is the horizontal one in our default writing mode. Then, we define overflow: hidden on the input element. This will make the input our scroller while the thumb is the subject.

If you forget about the overflow (so easy to forget!), another element will get used as the scroller, and won’t really know which one, and nothing will work as expected. Always remember that you need to define the subject using view-timeline and the scroller using overflow. I will repeat it again: don’t forget to define overflow on the scroller element!

Next, we define the animation:

@property --val {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0; 
}
label {
  timeline-scope: --thumb-view;
}
output {
  animation: range linear both;
  animation-timeline: --thumb-view;
}
@keyframes range {
  0%   { --val: var(--max) }
  100% { --val: var(--min) }
}

Let’s start with timeline-scope. This is yet another scoping issue that will give you a lot of headaches. With anchor positioning, we saw that an anchor is by default available everywhere on the page and we have to limit its scope. With scroll-driven animations, the scope is limited to the element where it’s defined (the subject) and its descendant so we have to increase the scope to make it available to other elements. Two different implementations but the same issue.

Never ever forget about scoping when working with both features. Sometimes, everything is correctly defined and you are only missing timeline-scope or position: relative somewhere.

Next we define an animation that animates an integer between the min and max variables, then link that animation with the timeline we previously defined using animation-timeline.

Why the max is at 0% and the min at 100%? Isn’t that backwards, you ask?

Intuitively, we tend to think “from left to right” but this looks like it’s “from right to left”. To understand this, we need to consider the “scroll” part of the feature.

I know that we don’t have scrolling in our case but consider the following example to better understand.

When you scroll the container “from left to right” you have a red circle that moves “from right to left”. We still have the “from left to right” behavior but it’s linked to the scroll. As for the content, it will logically move in the opposite direction “from right to left”.

When the scroll is at the left, the element is at the right and when the scroll is at the right, the element is at the left. The same logic applies to our thumb even if there is nothing to scroll. When the thumb is at the right edge, this is our 0% state and we need to have the max value there. The left edge will be the 100% state and it’s the min value.

The last step is to show the value using a pseudo-element and counter()

output::before {
  content: counter(num);
  counter-reset: num var(--val);
}

And we are done!

Wait a minute, the values aren’t good! We are not reaching the min and max values. For the first slider, we are supposed to go from 0 to 120 but instead, we have 9 and 111.

Another trick related to the scroll part of the feature and here is a figure to illustrate what is happening:

The movement of the thumb is limited to the input container (the scroller) but the 0% and 100% state are defined to be outside the scroller. In our case, the subject cannot reach the 0% and 100% since it cannot go outside but luckily we can update the 0% and 100% state:

We can either use animation-range to make both states inside the container:

output {
  animation: range linear both;
  animation-timeline: --thumb-view;
  animation-range: entry 100% exit 0%;
}

Or we consider view-timeline-inset with a value equal to the width of the thumb.

input[type="range" i]::-webkit-slider-thumb{
  anchor-name: --thumb;
  view-timeline: --thumb-view inline;
  view-timeline-inset: var(--s); /* --s is defined on an upper element and is used to define the size of the thumb */
}

The first method seems better as we don’t have to know the size of the thumb (the subject) but keep in mind both methods. The view-timeline-inset property may be more suitable in some situations.

Now our slider is perfect!

A lot of stuff to remember, right? Between the scoping issues, the range we have to correct, the overflow we should not forget, the min that should be at 100% and max that should be at 0%, etc. Don’t worry, I feel the same. They are new features with new mechanisms so it requires a lot of practice to get used to them and build a clear mental model. If you are a bit lost, that’s fine! No need to understand everything at once. Take the time to play with the different demos, read the doc of each property, and try things on your own.

Adding Motion

Now let’s move to the fun part, those silly wobbly animations. A tooltip that follows the thumb with dynamic content is good but it’s even better if we add some motion to it.

You may think this is gonna be the hardest part but actually it’s the easiest one, and here is the relevant code:

@property --val {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0; 
}
@property --e {
  syntax: '<number>';
  inherits: true;
  initial-value: 0; 
}
output {
  animation: range linear both;
  animation-timeline: --thumb-view;
  animation-range: entry 100% exit 0%;
}
output:before {
  content: counter(num);
  counter-reset: num var(--val);
  --e: var(--val);
  transition: --e .1s ease-out;
  rotate: calc((var(--e) - var(--val))*2deg);
}
@keyframes range {
  0%   { --val: var(--max) }
  100% { --val: var(--min) }
}

We add a new CSS variable --e with a number type. This variable will be equal to the --val variable. Until now, nothing fancy. We have two variables having the same value but one of them has a transition. Here comes the magic.

When you move the thumb, the animation will update the --val variable inside the output element. The pseudo-element will then inherit that value to update the content and also update --e. But since we are applying a transition to --e, it will not have an instant update but a smooth one (well, you know how transitions work!). This means that for a brief moment, both --e and --val will not be equal thus their difference is different from 0. We use that difference inside the rotation!

In addition to this, the difference can get bigger if you move the thumb fast or slow. Let’s suppose the current value is equal to 5. If you move the thumb rapidly to the value 50, the difference will be equal to 45 hence we get a big rotation. If you move to the value 7, the difference will be equal to 2 and the rotation won’t be that big.

Here is the full demo again so you can play with it. Try different speeds of movement and see how the rotation is different each time.

If you want to dig more into this technique and see more examples I advise you to read this article by Bramus.

Another Example

Let’s try a different idea.

This time, I am adjusting the tooltip position (and its tail) to remain within the horizontal boundary of the input element. Can you figure out how it’s done? This will be your homework!

For the tooltip part, I already did the job for you. I will redirect you again to my online collection where you can get the code of the tooltip shape. Within that code, I am already defining one variable that controls the tail position.

Conclusion

CSS is cool. A few years ago, doing such stuff with CSS would have been impossible. You would probably need one or two JavaScript libraries to handle the position of the tooltip, the dynamic content, the motion, etc. Now, all it takes is a few lines of CSS.

It’s still early to adopt those features and include them in real projects but I think it’s a good time to explore them and get an overview of what could be done in the near future. If you want more “futuristic” experimentation make sure to check my CSS Tip website where I regularly share cool demos!

]]>
https://frontendmasters.com/blog/custom-range-slider-using-anchor-positioning-scroll-driven-animations/feed/ 3 3569
How to Get the Width/Height of Any Element in Only CSS https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/ https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/#respond Thu, 25 Jul 2024 14:14:28 +0000 https://frontendmasters.com/blog/?p=3119 Getting the dimension of an element using JavaScript is a trivial task. You barely even need to do anything. If you have a reference to an element, you’ve got the dimensions (i.e. el.offsetWidth / el.offsetHeight). But we aren’t so lucky in CSS. While we’re able to react to elements being particular sizes with @container queries, we don’t have access to a straight up number we could use to, for example, display on the screen.

It may sound impossible but it’s doable! There are no simple built-in functions for this, so get ready for some slightly hacky experimentation.

Note: At time of writing, only Chrome (and Edge) have the full support of the features we will be using so consider those browsers to read the article.

Let’s start with a demo:

This demo has a simple layout with elements that will all have different sizes. Each rectangular element displays it’s own width/height. You can resize the browser or adjust the content; the values will update automatically.

Don’t try to find the hidden JavaScript, it’s 100% CSS magic, powered mostly by scroll-driven animations.

Why Scroll-Driven Animations?

Scroll-Driven animations is one of the most popular new CSS features in 2024. It unlocked a lot of possibilities and solved some common problems.

How are these features relevant to this situation of figuring out an element’s dimensions, though?

The terms “scroll” and “animation” tend to bring to mind, uhh, animating stuff on scroll. To be fair, that is the main purpose:

It allows you to animate property values based on a progression along a scroll-based timeline instead of the default time-based document timeline. This means that you can animate an element by scrolling a scrollable element, rather than just by the passing of time.

MDN

But we can think about it differently and achieve more than a simple animation on scroll. If you keep reading the MDN page, it explains there are two types of “scroll-based timelines”. In our case, we will consider the “view progress timeline”.

You progress this timeline based on the change in visibility of an element (known as the subject) inside a scroller. The visibility of the subject inside the scroller is tracked as a percentage of progress.

MDN

With this type of scroll timeline, there are three relevant elements: the scroller which is the container having the scroll, the subject which is an element moving inside the container and the animation that will progress based on the position of the “subject” inside the “scroller”.

The three elements are linked with each other. To identify the progress of the animation we need to know the position of the subject inside the scroller and for this, we need to know the dimension of the scroller, the dimension of the subject, and the offset of the subject (the distance between the subject and the edges of the scroller).

So our equation contains four variables:

  1. Dimension of the scroller
  2. Dimension of the subject
  3. Progress of the animation
  4. Offset of the subject

If three variables are known, we can automatically find the missing one. In our case, the missing variable will be the “dimension of scroller” and that’s how we are going to find the width/height of any element (an element that will a be scroller).

How Does it Work?

Let’s dive into the theory and get to how scroll-driven animations are actually used to do this. It won’t be long and boring, I promise! I’ll be using width as the dimension being measured, but height would use the same logic just on the other axis.

Consider the following figure:

We have a container (the scroller) and an element inside it (the subject) placed on the left. There are two special positions within the container. The 0% position is when the element is at the right (inside the container) and the 100% position is when the element has exited the container from the left (outside the container).

The movement of the subject between 0% and 100% will define the percentage of the progression but our element will not move so the percentage will be fixed. Let’s call it P. We also know the width of the subject and we need to find the width of the scroller.

Remember the variables we talked about. Considering this configuration, we already know three of them: “the width of the subject”, “the offset of the subject” (fixed to the left edge), and the “progress of the animation” (since the subject is fixed). To make things easier, let’s consider that the width of the scroller is a multiplier of the width of the subject:

W = N * S.

The goal is to find the N or more precisely, we need to find the relation between the P and N. I said the P is fixed, but in reality it’s only fixed when the scroller width is fixed which is logical. But if the width of the scroller changes, the progress will also change, that’s why we need to find the formula between the progress and the width.

Let’s start with the case where the width of the scroller is equal to twice the width of the subject, we get the following:

The subject is in the middle between 0% and 100% so the progress in this case is 50%. For N = 2 we get P = 50%.

Let’s try for N = 3:

Now we have two extra slots in addition to the 0% and 100%. If we suppose that the subject can only be placed inside one of the 4 slots, we can have the following progress: 0%33.33%66.67%100%. But the subject is always placed at the before-the-last slot so the progress in this case is equal to 66.67% or, seen differently, it’s equal to 100% - 100%/3 (100%/3 is the progression step).

Are you seeing the pattern? If the width of the scroller is equal to N times the width of the subject we will have N+1 slots (including 0% and 100%) so the step between each slot is equal to 100%/N and the subject is located at the before-the-last slot so the progress is equal to 100% - 100%/N.

We have our equation: P = 100% - 100%/N so N = 100%/(100% - P).

If we convert the percentage to values between 0 and 1 we get N = 1/(1 - P) and the width we are looking for is equal to W = N * S = S/(1 - P).

Now If we consider a width for the subject equal to 1px, we get W = 1px/(1 - P) and without the unit, we have W = 1/(1 - P).

Let’s Write Some Code

Enough theory! Let’s transform all this into code. We start with this structure:

<div class="container"></div>
.container {
  overflow: auto;
  position: relative;
}
.container:before {
  content: "";
  position: absolute;
  left: 0;
  width: 1px;
}

The scroller element is the container and the subject element is a pseudo-element. I am using position: absolute so the subject doesn’t affect the width of the container (the value we need to calculate). Like described in the previous section, it’s placed at the left of the container with 1px of width.

Next, we define a named timeline linked to the pseudo-element (the subject)

.container {
  timeline-scope: --cx;
}
.container:before {
  view-timeline: --cx inline
}

The MDN description of the property:

The view-timeline CSS shorthand property is used to define a named view progress timeline, which is progressed through based on the change in visibility of an element (known as the subject) inside a scrollable element (scroller). view-timeline is set on the subject.

We consider the inline (horizontal) axis. We need to also use timeline-scope to give the container access to the view progress. By default, a named timeline is scoped to the element where it’s defined (and its descendants) but we can change this to make it available at any level.

Why not define the scope at the html level, then?

Enlarging the scope to all the elements may sound like a good idea, but it’s not. We may need to use the same code for different elements so limiting the scope allows us to reuse the same code and keep the same naming.

I won’t spend too much time detailing the scope feature but don’t forget about it. If the code doesn’t work as intended, it’s probably a scoping issue.

Now let’s define the animation:

@property --x {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}
.container {
  animation: x linear;
  animation-timeline: --cx;
  animation-range: entry 100% exit 100%; 
}
@keyframes x {
  0%   { --x: 0; }
  100% { --x: 1; }
}

We define a keyframes that animates a variable from 0 to 1. We have to register that variable with a number type to be able to animate it. We run the animation on the container with a linear easing and define the timeline using animation-timeline.

At this step, we told the browser to consider the named timeline defined on the pseudo-element (the subject) as the reference for the animation progress. And that progress will be stored in the --x variable. At 50%, we have --x: 0.5, at 70%, we have --x: 0.7, and so on.

The last step is to add the formula we identified earlier:

@property --w {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0; 
}
.container {
  --w: calc(1/(1 - var(--x)));
}

The --w variable will contain the width in pixel of the container as a unitless value. It’s important to notice the “unitless” part. It gives us a lot of flexibility as we can integrate it within any formula. If you are a CSS hacker like me, you know what I mean!

What about that animation-range: entry 100% exit 100%;?

In addition to using a named timeline to define which element control the progress, we can also control the range of the animation. In other words, we can explicitly define where the 0% and 100% progress are located within the timeline.

Let’s get back to the first figure where I am showing the 0% and 100% progress.

The 0% is when the subject has completely entered the scroller from the right. We can express this using animation-range-start: entry 100%.

The 100% is when the subject has completely exited the scroller from the left. We can express this using animation-range-end: exit 100%.

Or using the shorthand:

animation-range: entry 100% exit 100%;

If you are new to scroll-driven animations, this part is not easy to grasp, so don’t worry if you don’t fully understand it. It requires some practice to build a mental model for it. Here is a good online tool that can help you visualize the different values.

Now, we do the same for the height and we are done. Here is the first demo again so you can inspect the full code.

Notice that I am using another pseudo-element to show the values. Let’s consider this as our first use case. Being able to get the width/height of any element and show them using only CSS is super cool!

.size::after {
  content: counter(w) "x" counter(h);
  counter-reset: w var(--w) h var(--h);
}

Are There Any Drawbacks?

Even if it seems to work fine, I still consider this as a “hack” to be used with caution. I am pretty sure it will fail in many situations so don’t consider this as a robust solution.

I also said “any element” in the title but in reality not all of them. It’s mandatory to be able to have a child element (the subject) so we cannot apply this trick to elements like <img> for example.

You also need to add overflow: auto (or hidden) to the container to make it the scroller for the subject. If you plan to have overflowing content then this solution will give you some trouble.

The value you will get using this method will include the padding but not the border! Pay attention to this part and compare the values you get with the ones of the Dev tools. You may need to perform another calculation to get the real dimension of the element by adding or subtracting specific amounts.

Another drawback is related to the use of 1px as our unit. We assumed that the size is a multiplier of 1px (which is true in most cases) but if your element is having a size like 185.15px, this trick won’t work. We can overcome this by using a smaller width for the subject (something like 0.01px) but I don’t think it is worth making this hack more complex.

A Few Use Cases

The first use case we saw is to show the dimension of the element which is a cool feature and can be a good one for debugging purposes. Let’s dig into more use cases.

Getting the Screen Dimension

We already have the viewport units vh and vw that works fine but this method can give us the unitless pixel values. You may ask how to do this since the viewport is not a real element. The solution is to rely on position: fixed applied to any element on the page. A fixed element is positioned relative to the viewport so its scroller will the viewport.

If you check the code, you will see that I am relying on the HTML pseudo-element for the subject and I don’t need to define any overflow or position on the HTML element. Plus the values are available globally since they are defined inside the HTML element!

For this particular case, I also have another CSS trick to get the screen dimension with an easier method:

Calculating the Scrollbar Width

There is a slight difference between the two screen width calculating methods above. The first demo will not include the scrollbar width if the page has a lot of content but the second one will. This means that If we combine both methods we can get the width of scrollbar!

Cool right? In addition to the screen dimension, you can also have the width of the scrollbar. Both values are available at root level so you can use them anywhere on the page.

If you want, you can also get the scrollbar width using a different method like I am detailing here: Get the scrollbar width using only CSS

Counting Stuff

All the calculations we did were based on the 1px size of the subject. If we change this to something else we can do some interesting counting. For example, if we consider 1lh (the height of the line box) we can count the number of lines inside a text.

Here is the version where you can edit the content. The number of lines will adjust based on the content you will enter.

Note how I am playing with the scope in this example. I am making the variable available at a higher level to be able to show the count inside a different element. Not only we can count the numbers of lines but we can also show the result anywhere on the page.

Can you think about something else to count? Share your example in the comment section.

Transferring Sizes

Being able to control the scope means that we can transfer the size of an element to another one on the page.

Here is an example where resizing the left element will also resize the right one!

Another important part of this trick is being able to get the width/height values as integer. This allows us to use them within any formula and append any unit to them.

Here is an example, where resizing the left element will rotate/scale the right one.

I have mapped the width with the rotation and the height with the scaling. Cool right? We can get the width/height of an element, have them as an integer, and transfer them to another element to do whatever we want. CSS is magic!

Conclusion

I hope you enjoyed this funny experiment. I still insist on the fact that it’s a hacky workaround to do something that was not possible using CSS. Use it for fun, use it to experiment with more CSS-only ideas but think twice before including this into a real project. Using one line of JavaScript code to get the dimension of an element is safer. Not all CSS-only tricks are a good replacement for JavaScript.

This said, if you find an interesting use case or you have another CSS-only experimentation where this trick can be useful, share it in the comment section.

If you’re interested in more experimentation with scroll-eriven animations check the following articles:

Article Series

]]>
https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/feed/ 0 3119
How Keyboard Navigation Works in a CSS Game https://frontendmasters.com/blog/how-keyboard-navigation-works-in-a-css-game/ https://frontendmasters.com/blog/how-keyboard-navigation-works-in-a-css-game/#comments Mon, 08 Jul 2024 14:34:58 +0000 https://frontendmasters.com/blog/?p=2936 We’re going to build a “Super CSS Mario” game where you can use a keyboard and the arrow keys to move Mario around. Go ahead and play it to check it out. Note there is no JavaScript at all, it’s just HTML and CSS.

Why bother with a CSS-only game?

Creating a CSS-only game is a fun exercise. Restricting yourself to only HTML & CSS allows you to discover and unlock CSS trickery you can add to your toolbox. That’s what happens for me!

You might think that limiting yourself to CSS for all the functionality of the game is useless. CSS is not designed for this sort of thing, it’s for layouts and design control. But doing unusual and unexpected things in CSS is a great way to practice, and will lead to a deeper understanding of the language, making you a better CSS developer all around.

Interactivity without a Mouse

Many pure CSS games you will see around are playable mostly with a mouse. They rely on interactive elements such as checkboxes and pseudo-classes like :hover:active, :checked, and so on. But with recent CSS features, a keyboard control game (beyond tabbing) is also doable using CSS!

Cool right? Stay with me if you want to know the secret behind creating this game (and a few others at the end).

At the time of writing this, only Chrome (and Edge) have the full support of the features we will be using so consider those browsers to read the article.

For the sake of simplicity, I will skip the aesthetic parts. I will mainly focus on the techniques used to build the game. The code of demos used in the article may differ slightly from the real code used in the game.

Let’s start with this basic setup:

We have a container with an overflowing content that will trigger both the vertical and horizontal scrolling. Nothing fancy so far but let’s not forget that, in addition to the mouse, we can scroll the container using the direction keys. Try it! Click the container (above) then use the keyboard to scroll inside it.

Now let’s add two more elements inside the overflowing div to have the following code:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
    </div>
  </div>
</div>

Then we make the .game element sticky so it doesn’t move when we scroll the container:

The magic touch now is to introduce scroll-driven animations to move our character. We can scroll the outer container but everything else stays fixed. By adding scroll-driven animations we can control the movement of Mario as we want.

It may sound tricky, but the code is pretty simple:

.mario {
  position: relative;
  top: 0%;
  left: 0%;
  animation: 
    x linear,
    y linear;
  animation-timeline: 
    scroll(nearest inline),
    scroll(nearest block);
}
@keyframes x { to { left: 100% } }
@keyframes y { to { top: 100%  } }

We have two animations. Each controls the movement in one direction (horizontal or vertical). Then we link them with the scrolling of the outer container. The x animation will follow the “inline” scroll (horizontal) and the y animation will follow the ”block” scroll (vertical).

In other words, the scrolling will define the progress of the animation. Try it:

We have our keyboard control!

We can still use the mouse to manipulate the scrollbars but if we hide them, the illusion is perfect!

.container {
  scrollbar-width: none;
}

You still need to click inside the container to get the focus before using the keyboard. I add the tabindex attribute to the main container so you can get the focus using the “tab” key as well.

The game can be playable using only the keyboard. Here is the link for the full game again to test it. Either use the mouse or click “tab” to start the game.

Adding The Coins

Could we add coins on the screen and then have Mario “collect” them when they touch? Well… no, not really. CSS does not have “collision detection” (yet?). So let’s fake it!

Since we’re controlling the location of Mario with animations, we can know where he is located. We are going to rely on this information to simulate collision detection between Mario and the coins.

To start, let’s place a coin inside the game board:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin"></div>
    </div>
  </div>
</div>

And style it like this:

.coin {
  position: absolute;
  inset: 0;
}
.coin:before {
  content: "";
  position: absolute;
  width: 50px;
  left: calc(50% - 25px);
  top: calc(50% - 25px);
  aspect-ratio: 1;
}

The .coin container will fill the whole area of .game (we will see later why) and its pseudo-element is the visible coin:

The coin is placed at the center and to reach the center Mario needs to scroll half the distance vertically and horizontally which means it needs to reach half the x and y animations.

We use this information to define new animations that we link to the coin element like this:

.coin {
  animation: 
    c-x linear,
    c-y linear;
  animation-timeline: 
    scroll(nearest inline),
    scroll(nearest block);
}
@keyframes c-x {
  0% , 44%  {--c-x: 0}
  45%, 55%  {--c-x: 1}
  56%, 100% {--c-x: 0}
}
@keyframes c-y {
  0% , 44%  {--c-y: 0}
  45%, 55%  {--c-y: 1}
  56%, 100% {--c-y: 0}
}

This is the same animation configuration we used for Mario. One animation is linked to the horizontal scroll and another one is linked to the vertical scroll. Each animation will control a variable that will be either 0 or 1 based on the keyframes percentage.

The coin is placed at the center so we need the variable to turn 1 when the animation is around 50%. I am considering an offset of 5% to illustrate the idea but in the real code, I am using more accurate values.

Now, we will introduce another CSS feature: style queries. It allows us to conditionally apply specific CSS based on the value of custom properties (CSS variables). Style queries require a parent-child relation, so that’s why the real coin is the pseudo element of .coin container.

.coin {
  container-name: c;
}
@container c style(--c-x: 1) and style(--c-y: 1) {
  .coin:before {
     /* do what you want here */
  }
}

The previous animations will make both variables equal to 1 at 50% (when Mario is at the center) and the style query will apply a specific CSS when both variables are equal to 1.

In the below example, when Mario is above the coin, a red background will appear. We have our collision detection!

As I said previously, this is not super accurate. I am keeping this simple to illustrate the idea. In the real code, I am using more precise calculations to get a perfect collision detection.

What we did until now is good but we need better. The red color is only visible when Mario touches the coin but we need a way to maintain this state. In other words, if it turns red, it should stay red.

To achieve this, we have to introduce another animation and update the code like below:

.coin {
  container-name: c;
}
.coin:before {
  animation: touch .1s forwards linear var(--s, paused);
}
@keyframes touch {
  1%, 100% { background-color: red; }
}
@container c style(--c-x: 1) and style(--c-y: 1) {
  .coin:before {
     --s: running
  }
}

We define an animation that is “paused” initially and when the condition is met we make it “running”. I am using a small duration and a forwards configuration to make sure the red color stays even when Mario moves away from the coin.

With this configuration, we can add an animation that makes the coin disappear (instead of just the color change).

To add more coins, we add more .coin elements with different positions and animations. If you check the real code of the game you will find that I am defining different variables and using Sass to generate the code. I am using a grid system where I can control the number of columns and rows and I am defining another variable for the number of coins. Then with the help of the random() function from Sass I can randomly place the coins inside the grid.

The important thing to notice is how the HTML code is organized. We don’t do the following:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin"></div>
      <div class="coin"></div>
      <div class="coin"></div>
      ...
    </div>
  </div>
</div>

But rather the following:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin">     
        <div class="coin">
          <div class="coin">
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

The .coin elements should not be siblings but nested inside each other. I need this configuration to later calculate the score. For this reason, a .coin element needs to take the whole area of the game to make sure its descendants will have access to the same area and we can easily place all the coins following the same code structure.

There is probably a way to make the game work by having the .coin elements as siblings but I didn’t focus on the HTML structure too much.

Calculating The Score

To calculate the score, I will add a last element that should also be nested within all the .coin elements. The nested configuration is mandatory here to be able to query all the .coin elements.

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin">     
        <div class="coin">
          <div class="coin">
           <div class="result"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

And here is the Sass code to illustrate how to calculate the score:

.result {
  $anim: ();
  @for $i from 1 to ($c+1) {
    --r#{$i}:0; 
    $anim: append($anim,'r#{$i} .1s forwards var(--s-r#{$i},paused)',comma);
    @container c#{$i} style(--c#{$i}-x: 1) and style(--c#{$i}-y: 1) {
       --s-r#{$i}: running
    }
    @keyframes r#{$i} {1%,to {--r#{$i}:1}}
  }
  $sum: ("var(--r1)");
  @for $i from 2 to ($c+1) {
    $sum: append($sum,'+ var(--r#{$i})' , space);
  }
  --sum: calc(#{$sum});
  animation: #{$anim};
}

For each coin, I will define one animation, one container query, and one @keyframe.

Notice how the configuration is similar to the one we used previously. When Mario touches the coin (--ci-x and --ci-y are equal to 1) we run an animation that will update the variable --ri from 0 to 1 and will maintain its value. In other words, a variable is incremented from 0 to 1 when a coin is touched and we have as many variables as coins in the game.

Then we define another variable that is the sum of all of them. That variable will contain the score of the game. To show the score we combine that variable with a counter and we rely on a pseudo-element like the below:

.result:before {
  content: "SCORE - " counter(r);
  counter-reset: r var(--sum);
}

Each time Mario collects a coin, the counter is reset with a new value, and the score is updated.

The Final Screen

To end the game, I will also rely on the sum variable and a style query. We test if the sum is equal to the number of coins. If that’s the case, we update some of the CSS to show the final screen.

The code will look like the below:

.result {
  container-name: r;
}
@container r style(--sum: #{$c}) {
  .result:after {
    /* the CSS of the final screen */
  }
}

For this style query, it’s important to register the sum variable using @property so that the browser can correctly evaluate its value and compare it with the number of coins.

@property --sum {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0
}

We don’t need to do this with the other variables as there is no calculation to be done. They are either equal to 0 or 1.

What about the timer?

I deliberately skipped that part to make it your homework. The timer is closely related to the final screen and I let you dissect the original code to see how it works (when it starts, when it stops, etc). You will see that it’s the easiest part of the game. It’s also an opportunity to inspect the other parts of the code that I skipped.

We are done! Now, you know the secret behind my “Super CSS Mario” game. With a clever combination of scroll-driven animations and style queries, we can create a CSS-only game playable with keyboard navigation.

Take the time to digest what you have learned so far before moving to the next sections. I will share with you two more games but I will get faster with the explanation since the techniques used are almost the same. If you are struggling with some of the concepts, give it another read.

Super CSS Mario II

Let’s update the previous game and increase its difficulty by adding some enemies. In addition to collecting the coins, you need to also avoid the Goombas. Play “Super CSS Mario II”

Adding enemies to the game may sound tricky but it’s pretty easy since touching them will simply stop the game. The enemies will share the same code structure as the coins. The only difference is that all of them will control one variable. If one enemy is touched, the variable is updated from 0 to 1 and the game ends.

The HTML code looks like below:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin">     
        <div class="coin">
          <div class="coin">
            <div class="enemy">
              <div class="enemy">
                <div class="result"></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Like the coins, I need to keep the nested structure to have the parent-child relation.

For the CSS code, I will have the following for the .result element.

.result {
  $anim: append($anim,'enemy .1s forwards var(--eeee,paused)',comma);
  --ee: 0;
  @for $i from 1 to ($e+1) {
    @container e#{$i} style(--e#{$i}-x: 1) and style(--e#{$i}-y: 1) {
      --eeee: running
    }
  }
  @keyframes enemy {1%,to {--ee:1}}
}

In addition to the previous animations defined for each coin, we add an extra animation that will control the variable --ee. All the style queries of the enemies will update the same animation which means if one of them is touched the variable will be equal to 1.

Then, for the final screen, we will have two conditions. Either the sum reaches the number of coins and you win or the enemy variable is equal to 1 and it’s a game over!

.result {
  container-name: r;
}
@container r style(--sum: #{$c}) {
  .result:after {
    /* you win */
  }
}
@container r style(--ee: 1) {
  .result:after {
    /* game over */
  }
}

Here is the Pen to see the full Sass code.

A CSS-only Maze game

One more game? Let’s go! This time it’s a maze game where the character needs to grab an object without touching the wall of the maze. Click to play the maze game.

The cool part about this game is that we have discrete movements, unlike the previous ones. It makes the game more realistic and similar to those retro games we enjoyed playing. The wall and the Dino are similar to the enemies and the coins of the previous game so I won’t detail them. I will focus on the movement and let you dissect the code of the other parts alone (here is the Pen).

Let’s start with the following demo:

Press the bottom arrow key to scroll and you will notice that the value will increment by a specific amount (it’s equal to 40 for me). If you keep tapping a lot of times, the value will keep increasing by the same amount.

This demonstrates that one click will always move the scroll by the same amount (as long as you don’t keep the key pressed). This information is what I need to create the discrete movement. If the game didn’t work well for you then it’s probably related to that value. In the Pen, you can update that value to match the one you get from the previous demo.

Now let’s suppose we want a maze with 10 columns and 5 rows. It means that we need 9 clicks to reach the last column and 4 clicks to reach the last row. The horizontal overflow needs to be equal to 360px=(40px*9) while the vertical overflow needs to be equal to 160px=(40px*4).

Let’s turn this into a code:

<div class="container">
  <div></div>
</div>
.container {
  width: 500px;  /* 50px * 10 */
  height: 250px; /* 50px * 5  */
}
.container div {
  width:  calc(100% + 40px*9);
  height: calc(100% + 40px*4);
}

The 50px I am using is an arbitrary value that will control the size of the grid.

Try to scroll the container using the keyboard and you will notice that you need exactly 9 clicks horizontally and 4 clicks vertically to scroll the whole content.

Then we can follow the same logic as the Mario game (the sticky container, the character, etc) but with a small difference: the x and y animations will animate integer variables instead of the top and left properties.

@property --x {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0
}
@property --y {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0
}
.character {
  width: 50px; /* the same value used to control the size of the grid */
  position: absolute;
  translate: calc(var(--x)*100%) calc(var(--y)*100%);
  animation: x linear,y linear;
  animation-timeline: scroll(nearest inline),scroll(nearest block);
}
@keyframes x { to { --x: 9 } }
@keyframes y { to { --y: 4 } }

We have a discrete keyboard movement using only CSS! Not only that, but thanks to the variable --x and --y we can know where our character is located within the grid.

You know the rest of the story, we apply style queries on those variables to know if the character hit a wall or if it reaches the Dino! I let you dissect the code as a small exercise and why not update it to create your own maze version? It could be a fun exercise to practice what we have learned together. Fork it and share your own maze version in the comment section.

Conclusion

I hope you enjoyed this CSS experimentation. It’s OK if you were a bit lost at times and didn’t fully understand all the tricks. What you need to remember is that scroll-driven animations allow us to link the scrolling progress to any kind of animation and style queries allow us to conditionally apply any kind of CSS based on the value of custom properties (CSS variables). Everything else depends on your creativity. I was able to create “Super CSS Mario” and a maze game but I am pretty sure you could do even better.

One day, someone will create a fully playable FPS using only CSS. Keyboard to move the character and mouse to kill enemies. Why not, nothing is impossible using CSS!

]]>
https://frontendmasters.com/blog/how-keyboard-navigation-works-in-a-css-game/feed/ 1 2936