Monday, December 8, 2025

Scrollytelling on Steroids With Scroll-State Queries


Learn you a narrative? What enjoyable would that be? I’ve obtained a greater concept: let’s inform a narrative collectively.

Photopia by Adam Cadre

Do you consider scrolling as a extra fashionable approach of studying than turning pages in a guide? Nope, the idea originated in historic Egypt, and it’s older than what we now classify as books. It’s primarily based on how our ancestors learn historic bodily scrolls, the earliest type of editable textual content within the historical past of writing. I’m Jewish, so I bear in mind my earliest non-digital scrolling expertise was horizontally scrolling the Torah, which will be extra immersive than historically scrolling a webpage. The bodily actions to navigate texts have captured the creativeness of many a storyteller, main authors to gamify the act of turning pages and to create tales that incorporate the bodily actions of opening a guide and turning pages as a part of the narrative. Nevertheless, revolutionary experiences utilizing non-standard scrolling haven’t been explored as completely.

Picture by Taylor Flowe on Unsplash

I can sympathize with those that dismiss scrollytelling as a gimmick: it may be an annoyance if it’s only for the sake of cleverness, however my favourite examples I’ve seen through the years inform tales we couldn’t in any other case. There’s one thing uniquely immersive about tales pushed by a mechanic that has lived in our species’ collective muscle reminiscence since historic days.

Nonetheless unconvinced of the worth of scrollytelling? Alright, hypothetical annoying skeptic, let’s first heat up with some widespread use circumstances for scroll-based styling.

It’s superior that Chrome has stable help for native scroll-driven animations with out requiring JavaScript, and we see that each Safari and Firefox are actively engaged on help for the brand new scroll-driven requirements. These new options facilitate optimized, easy scroll-driven animations. The help through pure CSS syntax makes scroll-driven animation a extra approachable choice for designers who could also be extra snug with CSS than with the equal JavaScript.

Certainly, although I’m a full-stack developer who’s speculated to know every thing, I discovered having scroll-driven animation constructed into the browser and out there with a number of traces of CSS will get my creativity flowing, inspiring me to experiment greater than if I needed to undergo hoops of a proprietary library and writing JavaScript, which previously would possibly embody messing with intersection observer and fiddly code.

If animation timelines weren’t sufficient, Chrome has now launched help for CSS carousel, scroll-initial-target, and scroll-state queries—all of which give alternatives to regulate scrolling behaviors in CSS and elegance all of the issues primarily based on scrolling.

In my view, scroll-state is extra of an evolutionary than revolutionary addition to the rising vary of scroll-related CSS options. Animation timelines are so highly effective that they are often hacked to realize lots of the similar results we will implement with scroll-state queries. Subsequently, consider scroll-state as a extremely handy, simplified subset of what we will do in additional verbose hacky methods with animation timelines and/or view timelines.

Some examples of results scroll-state simplifies are:

  1. Earlier than scroll-state queries existed, you possibly can hack view progress timelines to create scroll-triggered animations, however we now have snapped scroll-state queries to realize comparable results.
  2. Earlier than snappped queries existed, Bramus demonstrated a hack to simulate a hypothetical :snappped selector utilizing scroll-driven animations.
  3. Earlier than scrollable queries existed, Bramus confirmed how we might do comparable issues utilizing scroll-timeline.

Take a second to understand that Bramus is from the longer term, and to replicate on how scroll-state can simplify widespread UI patterns, reminiscent of scroll shadows, which Chris Coyier stated may be his “favourite CSS trick of all time.” This 12 months, Kevin Hamer confirmed how scroll-timeline can obtain scroll shadows in CSS with fewer tips. It’s glorious, however the one factor higher than intelligent CSS tips is that scroll shadows now not require a trick in any respect. Hacking CSS is enjoyable, however there’s something to be stated for that heat fuzzy feeling that CSS was made simply on your use case. This demo from the Chrome weblog reveals how scroll shadows and different visible affordances are straightforward to implement with scroll-state.

However the recognition of Kevin’s article suggests that standard, sane folks will gravitate to sensible use circumstances for the brand new CSS scroll-based options. The truth is, a standard and sane writer would possibly finish the article right here. Sadly, as I revealed in a earlier article, I’ve been cursed by a spooky shopkeeper who sells CSS tips at a haunted carnival, so I now roam the earth trying the unthinkable with pure CSS.

Determination time

As you attain this paragraph within the article, you understand that if you scroll, it fast-forwards actuality. Subsequently, after we finish the dialogue of scroll shadows, the shadows swallow the world exterior your window, besides for 2 glowing phrases hovering close to your own home: CSS TRICKS. You wander out by way of your entrance door and meet a avenue vendor standing beneath the neon signal. The letters give her a number of shadows as if she has thrown them down like discarded masks, undecided about which shade of evening to put on. On the desk earlier than her lies a weathered scroll. It unrolls by itself, whispering misremembered fragments from a forgotten CSS-Tips article: “A scroll set off is a degree of no return, like a entice sprung as soon as the hapless person scrolls previous a sure level.”

The neon sparkles like a glitch, revealing one other of the shopkeeper’s faces: a hearth demon doppleganger of your self who’s the villain of the CodePen we’ll descend into when you scroll additional.

“Will you proceed?” the hearth demon hisses. “Will you scroll deeper into the insanity on the far edges of CSS?

Non-linear scrollytelling

Evidently, you might be sport to play with fireplace, so take a look at the pure CSS experiment beneath, which demonstrates a way I name “nonlinear scrollytelling,” wherein the person controls the result of a visible story by deciding which route to scroll subsequent. It’s a scrolling Select Your Personal Journey. But when your browser is much less adventurous than you might be, watch the display screen recording as a substitute. The experiment will solely work on Chromium-based browsers for now, as a result of it depends on scroll-state, animation-timeline, scroll-initial-target and CSS inline conditionals.

I haven’t seen this system within the wild, so let me know within the feedback if in case you have seen different examples of the thought. For now, I’ll declare credit score for pioneering the mechanics — however I give credit score to the gifted Useless Revolver for creating the superior, inexpensive pixel artwork bundle I used for many of the graphics. The animated lightsaber icon was ripped from this cool CodePen by Ujjawal Anand, and I used ChatGPT to attract the climbable constructing. To make the unhealthy man, I reused the identical spritesheet from the participant character, however I applied the Mirror Match trope from Mortal Kombat, utilizing coloration shifting to create a “new” character who I evilized by casting the next spell in CSS:

.evil-twin {
  rework: rotateY(180deg);
  filter: invert(24%) sepia(99%) saturate(5431%) hue-rotate(354deg) brightness(93%) distinction(122%);
  background-image: url(/* similar spritesheet because the participant character */);
}

It’s cool that CSS helps recycle present property for these like me who’re drawing-challenged. I additionally needed to be sure that well-supported CSS options like rework and filter didn’t really feel overlooked of the enjoyable in an experiment stuffed with newer, emergent CSS options.

However when you’ve come this far, you’re in all probability keen to grasp the scroll-related CSS logic.

Our story begins in the course of the top

You will have observed our experiment earns additional loopy factors as quickly because it masses, by beginning on the center of the underside of the web page in order that the participant can select whether or not to scroll left to run away, or scroll proper to stroll unarmed in the direction of the unhealthy man if the participant needs to compete with the insanity degree of the sport’s creator.

This explainer for the emergent scroll-initial-target property reveals that controlling scroll place on load was beforehand attainable by hacking CSS animations and the scroll-snap-align property. Nevertheless, much like what we mentioned above concerning the worth proposition of scroll-state, a function like scroll-initial-target is thrilling as a result of it simplifies one thing that beforehand required verbose, fragile hacks, which might now get replaced with extra succinct and dependable CSS:

.spawn-point {
  place: absolute;
  left: 400vw;
  scroll-initial-target: nearest;
}

As cool as that is, we must always solely subvert expectations for a way a webpage behaves if we have now a ample cause. For example, CSS just like the above might have simplified my pure CSS swiper experiment, however Chrome solely added scroll-initial-target in February 2025, the month after I wrote that article. Utilizing scroll-initial-target can be justified within the swiper situation, because the crux of that design was that the person began within the center with the choice to swipe left or proper.

An identical dilemma is central to the opening of our scrollytelling narrative. The disorienting expertise of discovering ourselves in an sudden scroll place with solely the choice to scroll horizontally heightens the drama, because the person has to adapt to an uncommon approach of interacting whereas the unhealthy man quickly approaches. I’m feeling beneficiant, so let’s give the person 20 seconds to determine it out, however you’ll be able to experiment with completely different timeframes by modifying the --chase-time customized property on the prime of the supply file.

We’re going to create a CSS implementation of the slasher film trope wherein a strolling aggressor can’t be outrun. We do this by marking the unhealthy man as place: mounted, then including an infinite walk-cycle animation and one other animation that strikes him relentlessly from proper to left throughout the display screen. In the meantime, we give the participant character a working animation and place him primarily based on a horizontal animation timeline. He can run, however he can’t conceal.

physique {
  .idle {
    animation: idleAnim 1s steps(6) infinite;
  }

    /* --scroll-direction is populated utilizing the intelligent property Bramus demonstrates 
  right here https://www.bram.us/2023/10/23/css-scroll-detection */

  .sprite {
    rework: rotateY(calc(1deg * min(0, var(--scroll-direction) * 180)));
  }

  @container not model(--scroll-direction: 0) {
    .sprite {
      animation: runAnim 0.8s steps(8) infinite;
    }
  }

  .evil-twin-wrapper {
    place: mounted;
    backside: 5px;
    z-index: 1000;
    margin-left: var(--enemy-x-offset);
    /* we'll clarify later how we detect the best way the sport ought to finish */
    --follow: if(model(--game-state: ending): paused; else: working); 
    animation: var(--chase-time) forwards linear evil-twin-chase var(--follow);
  }
}

He can’t conceal, however we’ll subsequent introduce a second scroll-based resolution level utilizing scroll-state to detect when our hero has been backed right into a nook and see if we may also help him.

How scroll-state might save your life

As our hero runs away to the left, the buildings and sky within the cityscape background showcase a number of layers of parallax scrolling by assigning every layer an nameless animation timeline and an animation that strikes every layer sooner than the layer behind it.

.sky, .buildings-back, .buildings-mid, .sky-vertical, .buildings-back-vertical, .buildings-mid-vertical {
  place: mounted;
  prime: 0;
  left: 0;
  width: 800%;
  top: max(100vh, 300px);
  background-size: auto max(100vh, 300px);
  background-repeat: repeat-x;
  animation-timing-function: linear;
  animation-timeline: scroll(x);
}

/*...repetitively assign the corresponding animations to every layer...*/

@keyframes move-sky {
  from {
    rework: translateX(0);
  }
  to {
    rework: translateX(-2.5%);
  }
}

@keyframes move-back {
  from {
    rework: translateX(0);
  }
  to {
    rework: translateX(-6.25%);
  }
}

@keyframes move-mid {
  from {
    rework: translateX(0);
  }
  to {
    rework: translateX(-12.5%);
  }
}

This utilization of animation timelines is what they have been designed for, which is why the code is easy. If we needed to, we might push the boundaries and use the identical method to set a Houdini variable in an animation timeline to detect when the participant reaches the left nook of the display screen — however because of scroll-state queries, we have now a cleaner choice.

@container scroll-state((scrollable: left)) {
  physique {
    overflow-y: hidden;
  }
}

@container scroll-state((scrollable: backside)) {
  physique {
    width: 0;
  }
}

That’s all we have to toggle vertical and horizontal scrolling primarily based on place! That is the idea that permits the participant to flee from being slashed by the unhealthy man. Now we will scroll up and all the way down to climb the ladder solely when the participant reaches the left nook the place the ladder is, and disallow horizontal scrolling whereas he’s climbing.

I might have made the sport detect reaching the left of the display screen utilizing animation timelines, however that will contain customized property toggles, that are extra verbose and error-prone.

When the participant climbs to the highest of the ladder to gather the lightsaber, we do want one toggle property so the sport will bear in mind we have now collected the weapon, but it surely’s less complicated than if we had used animation timelines.

@keyframes collect-saber {
  from {
    --player-has-saber: false;
  }
  to {
    --player-has-saber: true;
  }
}

physique {
  animation: .25s forwards var(--saber-collection-state, paused) collect-saber;
}

@container scroll-state(not (scrollable: prime)) {
  physique {
    --saber-collection-state: working;
  }
}


@container model(--player-has-saber: true) {
  .sprite {
    background-image: url(/*fight spritesheet*/);
  }

  .lightsaber {
    visibility: hidden;
  }
}

Contrariwise, the animation cycle whereas the sprite is climbing the ladder is a job for animation-timeline used to assign an nameless vertical timeline to the participant sprite. That is utilized conditionally when our scroll-state question detects that the participant is between the underside and the highest of the ladder. It’s a pleasant instance of how animation timelines and scroll-state queries are good at various things, and work nicely collectively.

@container scroll-state((scrollable: prime) and ((scrollable: backside))) {
  .player-wrapper {
    .sprite {
      animation: climbAnim 1s steps(8);
      animation-timeline: scroll(root y);
      animation-iteration-count: 10;
    }
  }
}

End him with deadly conditionality

We apply the strategies I found in my CSS collision detection article to detect when the 2 characters meet for his or her showdown. At that time, we need to disable scrolling completely and show the suitable non-interactive endgame cutscene relying on the alternatives our person made. Discover that if we detect the nice man gained, he solely strikes with the sword as soon as, whereas the unhealthy man will proceed to slash infinitely, even after the nice man is lifeless. What can I say — I used to be engaged on this CodePen round Halloween.

Up to now, I wrote an article questioning the necessity for inline CSS conditionals — however now that they’ve landed in Chrome, I discover them addictive, particularly when making a closely conditional CSS experiment like nonlinear scrollytelling. I wish to think about that the brand new if() perform stands for Interactive Fiction. Under is how I detect the endgame situations and select which animations to play within the ultimate cutscene. I’m not positive of essentially the most readable option to house out if() code in CSS, so be happy to begin holy wars on that subject within the feedback.

physique {
  --min-of-player-and-enemy-x: min(var(--player-x-offset), var(--enemy-x-offset) - 10px);
  --max-of-player-and-enemy-y: max(var(--player-y-offset, 5px));
  --game-state:
    if(
      model(--min-of-player-and-enemy-x: calc(var(--enemy-x-offset) - 10px)) and elegance(--max-of-player-and-enemy-y: 5px): 
        ending; 
      else: 
        enjoying
    );
  overflow:
    if(
      model(--game-state: ending): 
        hidden; 
      else: 
        scroll
    );
}

@container model(--player-has-saber: true) and elegance(--game-state: ending) {
  .player-wrapper {
    .sprite {
      animation: assault 0.7s steps(4) forwards;
    }

  .speech-bubble {
    animation: show-endgame-message 3s linear 1s forwards;

    &::earlier than {
      content material: 'Refresh the web page to play once more';
    }
  }
    
  .evil-twin-wrapper {
    .evil-twin {
      evil-twin-die 0.8s steps(4) .7s forwards;
    }
  }
}

@container model(--player-has-saber: false) and elegance(--game-state: ending) {
  .player-wrapper {
    .sprite {
      animation: player-die .8s steps(6) .7s forwards;
    }
  }

  .evil-twin-wrapper {
    .speech-bubble {
      animation: show-endgame-message 3s linear 1s forwards;
      show: block;

      &::earlier than {
        content material: 'Baha! Refresh the web page to struggle me once more';
      }
      .evil-twin {
        assault 0.8s steps(4) infinite;
      }
    }
  }
}

Ought to we non-linearly scrollytell all of the issues?

I’m glad you requested, hypothetical troll who wrote that heading. After all, even placing the technical challenges apart, you realize that this gained’t all the time be the correct method for an internet site. As Andy Clarke not too long ago identified right here on CSS-Tips, design is storytelling. The wants of each story are completely different, however I discovered my little pixel artwork man’s emotional story arc requires non-linear scrollytelling.

I feel this specific instance isn’t a gimmick and is a official type of net design expression. The demo tells a easy story, however my spouse identified {that a} private scenario I’m coping with has sturdy analogies to the pixel man’s journey. He finds himself in a scenario the place the one sane choice is to permit himself to be backed right into a nook, however when all appears misplaced, he finds a option to rise above the adversity. Then he learns that the ethical excessive floor is its personal type of entice, so he should put his personal spin on the knowledge of Solar Tzu that “to know your enemy, you should turn into your enemy.” He apparently lowers himself again to the aggressor’s degree — however he solely does what is important. The bitterwseet ethical is that survival typically requires taking a leaf out of the enemy’s guide — however the person has been guiding the hero by way of this story, which helps the viewers to grasp that the nice man’s motivations usually are not akin to these of his adversary. Whereas testing the CodePen, I discovered the story transferring and even suspenseful in an 8-bit nostalgia form of approach, even when a few of that suspense was my uncertainty about whether or not I might get it working.

From a technical viewpoint, I feel constructing a full-scale web site primarily based on this concept would require a mixture of CSS and JavaScript, as a result of storing state in CSS presently requires hacks (like this one, which is cool but additionally extremely experimental). The paused animation method to do not forget that the participant collected the sword can glitch because of timer drift, so there’s a small probability the dude will begin the sport with the lightsaber already in his hand! If you happen to resize the window throughout the endgame, you’ll be able to glitch the sport, after which issues get actually bizarre. Against this, one thing just like the scroll snap occasions — already supported in Chrome — would enable us to retailer state and even play sounds utilizing a script that fires primarily based on scroll interactions.

It looks as if we have already got sufficient in CSS to construct a website like this one, which makes use of horizontal multimedia scrollytelling to lift consciousness that interpersonal violence exists on a continuum and tends to escalate if the goal is unable to acknowledge the early warning indicators. That’s a worthy subject I sadly have some expertise with, and the utilization of horizontal scrollytelling to deal with it demonstrates that all kinds of tales will be advised engagingly by way of scrollytelling.

I go away to the assorted futures (to not all) my backyard of forking paths.

Jorge Luis Borges

Related Articles

Latest Articles