Tuesday, December 2, 2025
HomeProgrammingScrollytelling on Steroids With Scroll-State Queries

Scrollytelling on Steroids With Scroll-State Queries


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

Photopia by Adam Cadre

Do you consider scrolling as a extra fashionable method of studying than turning pages in a guide? Nope, the idea originated in historical Egypt, and it’s older than what we now classify as books. It’s based mostly on how our ancestors learn historical 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. Nonetheless, modern experiences utilizing non-standard scrolling haven’t been explored as completely.

Photo of an ancient scroll partially unrolled inside a glass case on a wooden desk.
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 historical days.

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

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

Certainly, regardless that I’m a full-stack developer who’s purported to know the whole lot, I discovered having scroll-driven animation constructed into the browser and obtainable with just a few strains 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 may embody messing with intersection observer and fiddly code.

If animation timelines weren’t sufficient, Chrome has now launched assist 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 based mostly on scrolling.

For my part, 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 most of the similar results we are able to implement with scroll-state queries. Due to this fact, consider scroll-state as a extremely handy, simplified subset of what we are able to 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 may hack view progress timelines to create scroll-triggered animations, however we now have snapped scroll-state queries to realize related 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 related issues utilizing scroll-timeline.

Take a second to understand that Bramus is from the longer term, and to mirror on how scroll-state can simplify frequent UI patterns, corresponding to 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 methods. It’s wonderful, however the one factor higher than intelligent CSS methods is that scroll shadows 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 in your use case. This demo from the Chrome weblog reveals how scroll shadows and different visible affordances are simple to implement with scroll-state.

However the reputation of Kevin’s article suggests that ordinary, sane folks will gravitate to sensible use circumstances for the brand new CSS scroll-based options. In truth, a standard and sane writer may finish the article right here. Sadly, as I revealed in a earlier article, I’ve been cursed by a spooky shopkeeper who sells CSS methods at a haunted carnival, so I now roam the earth making an attempt the unthinkable with pure CSS.

Determination time

As you attain this paragraph within the article, you notice that whenever you scroll, it fast-forwards actuality. Due to this fact, 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 via 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 some extent of no return, like a lure sprung as soon as the hapless person scrolls previous a sure level.”

The neon glints 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’re recreation to play with fireplace, so try the pure CSS experiment under, which demonstrates a way I name “nonlinear scrollytelling,” through which the person controls the end result of a visible story by deciding which course to scroll subsequent. It’s a scrolling Select Your Personal Journey. But when your browser is much less adventurous than you’re, watch the display 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 in case you have seen different examples of the concept. For now, I’ll declare credit score for pioneering the mechanics — however I give credit score to the proficient Lifeless 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 {
  remodel: 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 current property for these like me who’re drawing-challenged. I additionally wished to guarantee that well-supported CSS options like remodel and filter didn’t really feel unnoticed of the enjoyable in an experiment crammed with newer, emergent CSS options.

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

Our story begins in the course of the top

You might have seen our experiment earns additional loopy factors as quickly because it hundreds, 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 direction of the unhealthy man if the participant needs to compete with the insanity stage 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. Nonetheless, just like what we mentioned above in regards to the worth proposition of scroll-state, a characteristic 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 should always solely subvert expectations for the way a webpage behaves if we’ve a ample cause. As an 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 could be justified within the swiper situation, for the reason that 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 method 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 may experiment with completely different timeframes by enhancing the --chase-time customized property on the high of the supply file.

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

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 {
    remodel: 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: fastened;
    backside: 5px;
    z-index: 1000;
    margin-left: var(--enemy-x-offset);
    /* we'll clarify later how we detect the way in which 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 disguise, however we’ll subsequent introduce a second scroll-based determination 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 just a few layers of parallax scrolling by assigning every layer an nameless animation timeline and an animation that strikes every layer quicker than the layer behind it.

.sky, .buildings-back, .buildings-mid, .sky-vertical, .buildings-back-vertical, .buildings-mid-vertical {
  place: fastened;
  high: 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 {
    remodel: translateX(0);
  }
  to {
    remodel: translateX(-2.5%);
  }
}

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

@keyframes move-mid {
  from {
    remodel: translateX(0);
  }
  to {
    remodel: 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 — however because of scroll-state queries, we’ve 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 based mostly on place! That is the idea that permits the participant to flee from being slashed by the unhealthy man. Now we are able to scroll up and right 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 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’ve collected the weapon, but it surely’s easier 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: high)) {
  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 effectively collectively.

@container scroll-state((scrollable: high) 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 wish 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 useless. 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 circumstances and select which animations to play within the remaining cutscene. I’m not positive of essentially the most readable solution to area out if() code in CSS, so be at liberty to start out 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. In fact, even placing the technical challenges apart, you recognize that this gained’t all the time be the suitable method for a web site. As Andy Clarke just lately 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 believe this explicit instance isn’t a gimmick and is a official type of internet design expression. The demo tells a easy story, however my spouse identified {that a} private state of affairs I’m coping with has sturdy analogies to the pixel man’s journey. He finds himself in a state of affairs 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 solution to rise above the adversity. Then he learns that the ethical excessive floor is its personal type of lure, so he should put his personal spin on the knowledge of Solar Tzu that “to know your enemy, you have to develop into your enemy.” He apparently lowers himself again to the aggressor’s stage — however he solely does what is important. The bitterwseet ethical is that survival generally requires taking a leaf out of the enemy’s guide — however the person has been guiding the hero via 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 shifting and even suspenseful in an 8-bit nostalgia sort of method, even when a few of that suspense was my uncertainty about whether or not I might get it working.

From a technical viewpoint, I believe constructing a full-scale web site based mostly on this concept would require a mixture of CSS and JavaScript, as a result of storing state in CSS at present requires hacks (like this one, which is cool but in addition 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 likelihood the dude will begin the sport with the lightsaber already in his hand! If you happen to resize the window in the course of the endgame, you may glitch the sport, after which issues get actually bizarre. In contrast, one thing just like the scroll snap occasions — already supported in Chrome — would permit us to retailer state and even play sounds utilizing a script that fires based mostly on scroll interactions.

It looks as if we have already got sufficient in CSS to construct a web site like this one, which makes use of horizontal multimedia scrollytelling to boost 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 handle it demonstrates that all kinds of tales will be advised engagingly via scrollytelling.

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

Jorge Luis Borges

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments