Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Tue, 30 Jul 2024 21:03:48 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 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.

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.

I will end this article with a last demo where I am transforming the native progress element into a circular one. Can you figure out how it works? I am using the same technique. This time, I know both the width of the scroller and the subject and the missing variable is the progress. Try to dissect the code as homework 😜.

]]>
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
Using CSS Scroll-Driven Animations for Section-Based Scroll Progress Indicators https://frontendmasters.com/blog/using-css-scroll-driven-animations-for-section-based-scroll-progress-indicators/ https://frontendmasters.com/blog/using-css-scroll-driven-animations-for-section-based-scroll-progress-indicators/#comments Fri, 10 May 2024 12:50:18 +0000 https://frontendmasters.com/blog/?p=2134 Scroll-Driven Animations allow you to control animations based on the scroll progress of any particular element (often the whole document), or, a particular element’s visibility progress within the document. These are view() and scroll() animations, respectively. Both useful! It can be useful to apply the animation directly to the element itself, for instance, a <section> sliding into place as it enters the viewport. That kind of thing is cool and useful, but have you thought about extending the effects of these animations beyond the elements triggering them?

In CSS, the scroll-driven animations are effectuated using a couple of animation-timeline functions: scroll() and view(). You can learn more about them here.

In this article, we’ll use a view() animation, combined with a CSS custom property declared with @property to create a “currently-viewing” and section-based progress indicator for each section of a page. This kind of thing can be useful, for example, for a long documentation page so a user can see where they are in it, and how far through their current section they are. Kind of like a reading-progress bar, but smarter, as it is aware of individual page sections.

Here’s a demo:

The view() timeline will end up keeping track of each section’s position throughout scrolling, and @property helps pass down an animate-able result of each section’s scroll progress to its indicator element.

The HTML Foundation

To get started, let’s begin by laying down the HTML elements. We’re going to need:

  1. Page <section>s
  2. Scroll progress indicator bars

Here are both:

<section id="one">
    Section number one
    <span>First</span>
</section>

<section id="two">
    Second section
    <span>Second</span>
</section>

<section id="three">
    Third section
    <span>Third</span>
</section>

<section id="four">
    Final section
    <span>Fourth</span>
</section>

The <span>s above are the indicator elements, soon to be moved to the top-right corner of the viewport where they will remain fixed as a user scrolls through the page.

The CSS for the Sections and Indicators

section {
    width: 400px;
    aspect-ratio: 1 / 2;
    /* ... */
}
span {
    position: fixed;
    height: 1lh;
    line-height: 40px;
    width: 100px;
    right: 60px;
    --t: 60px; /* top variable */
    --h: calc(1lh + 10px); /* for the gap between spans */
    section:nth-of-type(1) &{
        top: var(--t);
    }
    section:nth-of-type(2) &{
        top: calc(var(--t) + var(--h));
    }
    section:nth-of-type(3) &{
        top: calc(var(--t) + 2 * var(--h));
    }
    section:nth-of-type(4) &{
        top: calc(var(--t) + 3 * var(--h));
    }
    &::before { /* the blue bar */
        display: block;
        position: absolute;
        content: '';
        width: 4px;
        height: inherit;
        background: rgb(55,126,245);
        /* ... */
    }
}

The spans are given position: fixed, and a right value, to fix them to the side of the screen. The top value of each span is measured (using calc()) based on their height and the gap in-between them. You may want to consider logical property alternatives to these values if it’s reasonable the page you’re working on could be translated.

A pseudo-element on the span is used as the actual progress indicator bar, a darker blue line that grows/shrinks on each indicator box.

Here’s a demo of that (with no animations yet, we’ll get there!)

Let’s proceed to the fun part — the animation!

The Scroll-Driven Animation

We’ll show the scroll progress of each section by animating the height of each bar indicator as the user scrolls through it. Well, perhaps not the height, the CSS property itself, but the visual height. We’ll actually use scaleY() on the bar, as that’s generally considered more performant to animate. This scaleY() function takes a number data type value, hence our need to declare that with @property. So, let’s register a custom property that takes a number value and set up a @keyframes animation that actually does the work:

@property --n {
    syntax: "<number>";
    inherits: true;
    initial-value: 0;
}
@keyframes slide {
    from { --n: 0; }
    to { --n: 1; }
}

It’s important that the inherits attribute is true when declaring the custom property. This ensures the value is accessible by the spans even though the animation- properties are added to the sections. The span inherits the value, as it were.

section {
    animation-timeline: view(block 98% 2%);
    animation-name: slide;
    animation-fill-mode: both;
    /* remaining code from the 1st css snippet goes here */
}
span {
    /* remaining code from the 1st css snippet goes here */
    &::before { /* the blue bar */
        /* remaining code from the 1st css snippet goes here */
        transform: scaleY(var(--n)); /* animation takes place here */
        transform-origin: top;
    }
}

I prefer the scroll progress be measured against a (conceptual) horizontal line that’s close to the bottom of the screen. Each time a section passes through this line, its animation timeline moves forward or backward based on the scrolling direction (up or down). The area I’d chosen for this is 2% from the bottom of the screen. Here’s the breakdown of the view() function’s values.

  1. block — the area is defined across the block axis. The block axis is the vertical axis for left to right text direction
  2. 98% — the area being defined begins at 98% from the top of the screen (or 98% from the start of the block axis)
  3. 2% — the area ends at 2% from the bottom of the screen (or 2% from the end of the block axis)

Since I wanted the defined area to be a line, I didn’t leave any space between the beginning and end of the area. You can, however, broaden the area if you wish. For instance, 70% 20% gives you a 10% long area on the screen for the scroll progress to be measured against. Or you can even move the area to the top of the screen. 0 100% will assign the very top of the viewport as the marker to help track the scroll progress.

The progress timeline then moves along the keyframe animation that we named slide, updating the custom variable --n with a corresponding value between 0 and 1. We’re using 0 and 1 here because 0 means “scale this bar all the way down to 0% tall” and 1 means “scale this bar all the way up to 100% tall”.

The transform-origin: top sets up the scaling to take place from the top of the bar element, meaning the bar will look like it “grows” from top to bottom which mimics how scrolling happens.

Here’s the final outcome:

A Variation

If you prefer that there be no bar for the first section initially, at least until after you’ve scrolled down a bit, limit the first section‘s animation range to a desirable potion. This might feel better to you (and/or users) because if they haven’t scrolled at all it might be weird to visually show a section as partially complete. Here’s that aleration:

section:nth-of-type(1) {
  animation-range: contain 70%;
}

The animation won’t begin until the 70% mark of the first section crosses the area on the screen defined by view(). The effect is observed when the viewport is shorter than the first section. Adjust as needed.

Conclusion

That’s it! Hopefully the article gives you an idea on how we can cascade the scroll position value and animation progress beyond a directly-assigned element, allowing us to build more dynamic scroll-based designs with just CSS. Knowing that you can pass a custom property value down to other elements, you might think about how style queries could play into that idea 🤔.

]]>
https://frontendmasters.com/blog/using-css-scroll-driven-animations-for-section-based-scroll-progress-indicators/feed/ 1 2134
A Practical Introduction to Scroll-Driven Animations with CSS scroll() and view() https://frontendmasters.com/blog/a-practical-introduction-to-scroll-driven-animations-with-css-scroll-and-view/ https://frontendmasters.com/blog/a-practical-introduction-to-scroll-driven-animations-with-css-scroll-and-view/#respond Thu, 08 Feb 2024 18:49:05 +0000 https://frontendmasters.com/blog/?p=762 If you were going to read one article to get you going with Scroll-Driven Animations from scratch, I think Adam Argyle has the one so far.

]]>
https://frontendmasters.com/blog/a-practical-introduction-to-scroll-driven-animations-with-css-scroll-and-view/feed/ 0 762
A Scroll-Driven Animation… with a Duration https://frontendmasters.com/blog/a-scroll-driven-animation-with-a-duration/ https://frontendmasters.com/blog/a-scroll-driven-animation-with-a-duration/#respond Tue, 30 Jan 2024 23:11:58 +0000 https://frontendmasters.com/blog/?p=692 Ryan Mulligan digs into a Jhey Tompkins Pen and discovers a neat little trick involving two @keyframe animations. The first is a “trigger” animation that triggers when a certain element comes into view, and flips a --trigger custom property. Via a Style Query, that triggers the actual animation.

Why bother? Scroll-driven animation durations are, well… scroll… driven. This trick severs that, so you can do stuff like “when this element comes into view, run this little animation over 600ms”, entirely in CSS.

]]>
https://frontendmasters.com/blog/a-scroll-driven-animation-with-a-duration/feed/ 0 692
Highlight Text When a User Scrolls Down to That Piece of Text https://frontendmasters.com/blog/highlight-text-when-a-user-scrolls-down-to-that-piece-of-text/ https://frontendmasters.com/blog/highlight-text-when-a-user-scrolls-down-to-that-piece-of-text/#comments Tue, 23 Jan 2024 22:41:42 +0000 https://frontendmasters.com/blog/?p=639 element in HTML, which feels right. […]]]> I was reading a great post on Lene Saile’s blog and noticed a cool little design feature on her site that highlights a line of text once you scroll to it. Here’s a video so you can see what I mean:

The highlighted line is done with a <mark> element in HTML, which feels right. I noticed the class name on Lene’s implementation is .gsap-highlight which implies GSAP is used which has as a great Scroll Trigger plugin. Let’s do this without JavaScript though, especially now that I’m hip to Scroll-Driven Animations.

Basic HTML

A paragraph with a mark (with a class):

<p>Lorem, ipsum dolor sit amet <mark class="scroll-highlight">consectetur adipisicing elit</mark>. Magnam voluptas aliquid, distinctio voluptatum neque qui modi. In adipisci ratione id officiis nulla veritatis, porro explicabo illum laudantium iure eius velit!</p>

CSS Mark Styling in CSS

I just want a solid background on the mark. But I’m not going to use background-color. Instead I’m going to use background-image, because then I can control the background-size which I ultimately want to animate. So:

mark.scroll-highlight {
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-color: transparent;
  background-image: linear-gradient(purple, purple);
}

Now I can animate that background-size from 0% 100% to 100% 100% which is the look we’re after. It even works when the text breaks across lines which is a miracle.

Now it’s a matter of when to run the animation.

Scroll-Driven Animation for the Mark

Here’s the whole trick:

mark.scroll-highlight {
  background-size: 0 100%;
  background-repeat: no-repeat;
  background-color: transparent;
  background-image: linear-gradient(purple, purple);

  animation: mark-it linear;
  animation-fill-mode: forwards;
  animation-timeline: view();
  animation-iteration-count: 1;
  animation-range: contain 0% contain 25%;
}

@keyframes mark-it {
  0% {
    background-size: 0 100%;
  }
  100% {
    background-size: 100% 100%;
  }
}

The coolest part to me is the animation-range which gives us the opportunity to say when to start and end the animation with a solid amount of control. In the code above we’re saying to start the animation as soon as the element is fully within the viewport, then finish when it’s 25% of the way up the currently visible viewport.

Here the element is maybe 20% up the current viewport, so the animation is 80% finished.

Demo

Remember native support for Scroll-Driven Animations is essentially Chrome ‘n’ friends only right now. You could easily treat this as a progressive enhancement, wrapping the animation stuff in a @supports (animation-timeline: view()) { } block or however you wanna do it.

Demo video:

]]>
https://frontendmasters.com/blog/highlight-text-when-a-user-scrolls-down-to-that-piece-of-text/feed/ 2 639
Background Size Zooming with Scroll Driven Animations https://frontendmasters.com/blog/background-size-zooming-with-scroll-driven-animations/ https://frontendmasters.com/blog/background-size-zooming-with-scroll-driven-animations/#comments Thu, 04 Jan 2024 15:16:33 +0000 https://frontendmasters.com/blog/?p=330 I’ve seen all the excitement about scroll-driven animations. I’m excited too. Animations that use scroll position as a timeline rather than a time-based duration is just a clever idea. It’s something that websites have been using probably-too-much JavaScript for for a long time and it’s nice to see that kind of thing move to CSS where it belongs.

But I haven’t gotten a chance to play with it directly very much yet. So here I go!

This all got started because I got a nice email from a reader who had a very specific effect in mind they were trying to replicate on their own website. Somewhat inspired by Chung-Yun Yoo’s website, the idea is: images that zoom in (or out) as you scroll. They don’t change dimensions themselves, they just change how much of the image you can see in the space they already occupy. This doesn’t require too much trickery — you just set the width, height, and background-size and you can affect how much of an <img> you can see. Changing that background-size has the zooming affect.

This is what we’re after (video):

The trick is changing the background-size on scroll. This is normally something you’d use JavaScript for, like scroll event listeners on the document and/or IntersectionObserver stuff. But now, we can express what we want in Scroll-Driven Animations.

Bramus Van Damme has lots of good resources on Scroll-Driven Animations like this article and the site scroll-driven-animations.style. The tool I found most helpful when trying to figure this out was this View Progress Timeline tool. In this tool, you can see how a green box has an animation run only during the time when it’s visible at all in the viewport. I was very pleased to have that click for me, as before I saw this I thought that the timelines needed to be the entire scroll-length of the element. Good job, API!

Note the cover keyword there that is helping us achieve that effect. It’s definitely worth a play with the other values to see what they do. Honestly I can imagine they are all useful in certain situations. Seeing the generated code, I was able to see that our image effect is quite simple to scaffold:

.img {
  animation-range: cover 0% cover 100%;
  animation: the-animation linear;
  animation-timeline: view();
  animation-iteration-count: 1;
}

@keyframes the-animation {
  from {
    /* whatever */
  }
  to {
    /* whatever */
  }
}

In our case, we can flesh out the animation with just some background-size changes and we’re good! Here’s a basic demo you can scroll up and down:

As I write, scroll-driven animations are available in Chrome ‘n’ friends, flagged in Firefox, and not yet available in Safari. There is a polyfill, which that demo above loads, so it should work across all three.

Those are the basics that achieve what I had in mind. Now here’s a Pen that uses the same animation across multiple elements, but sets where the animate to via CSS Custom Property.

Funnnnn.

]]>
https://frontendmasters.com/blog/background-size-zooming-with-scroll-driven-animations/feed/ 1 330