Scroll-Driven State Transfer

By admin

S croll-
PERSON

Driven State Transfer

In my

fourth
ORDINAL

article about scroll-driven animations, I explore how we can transfer the state of

one
CARDINAL

element to a completely different place on a page by connecting them with a unique identifier in

CSS
ORG

via a timeline-scope.

I ntroduction


Today
DATE

’s technique is a variation of an effect that I previously demonstrated a few times:

Side note: With this article’s release, I’m replacing this method with the new one. This is an example of such a sidenote! The method works only in

Chromium
ORG

-based browsers for now, but I like it more than the very hacky and not very accessible method that I used to have. Jump to this sidenote’s context.

The gist of the effect I’m talking about is an ability to mirror a particular state of some element — for example, hovered or focused — to an element in a different place on the page without a common or unique ancestral element that could have been used to deliver that stateGo to a sidenote.

Side note: If we have the same ancestral element and can target our elements in some way, like placing pre-defined classes, we could implement this with the :has() selector, but for any new values we’d have to modify the stylesheet afterwards. Jump to this sidenote’s context.

The minimal API that I want in the end is to be able to connect

two
CARDINAL

elements via a shared unique identifier without modifying the global CSS stylesheet — without adding new selectors and rules.


Today
DATE

, as you could have guessed from the article name, I’ll implement this effect with scroll-driven animations.

D isclaimers

At the moment of writing, scroll-driven animations are implemented only in

Chromium
ORG

-based browsers, but I am providing videos for all the examples, allowing you to see how they work. However, for the best experience, try opening the article in

Chrome
ORG

, Edge or Opera; it can be fun to play with the examples there! This is my

fourth
ORDINAL

articleGo to a sidenote on the topic of

Scroll-Driven Animations
ORG

. While the method I’m talking about today does not directly follow what I wrote before, I would skip a lot of the links to this

CSS
ORG

feature’s specs and assume you know what they are in general. In the examples, I’m using this effect to connect the elements in various ways, but that connection is only visual. If that technique is to be used in production (which I do not recommend until it lands in all browsers), then based on the use case, you’d need to think about how that connection could be conveyed in non-visual ways, like by using the aria-describedby and alike. If you know how this can be done properly and are willing to share it with me, anything would be welcome, as I’m not an accessibility expert. There is a chance that in the future we would be able to use the values from the HTML attributes as parts of the idents in CSS (see this

CSSWG
ORG

issue by

Bramus
ORG

), in which case we could use the values of the id , aria-describedby and other attributes to construct the idents in

CSS
ORG

and connect the elements without using additional custom properties.

T he Core Technique

Let me take the “

Cross-Referencing
WORK_OF_ART

” example from my article about anchor positioning and replace its method with scroll-driven animations instead:

Which property corresponds with which value? display visibility opacity hidden

0
CARDINAL

none

Toggle to a videolive example Hovering or focusing on various items in this example would highlight the other corresponding elements.

The HTML for that example is exactly the same as for the anchor-positioning example: the only thing we need in order to connect our elements are –is and –for custom properties with the dashed idents as values.

And here is the whole CSS that is responsible for the technique in that example:

@keyframes –is-active–example1 { entry

0%
PERCENT

, exit

100%
PERCENT

{ –is-active: initial; } } .example1 [

style*=’–is
PERSON

:’] { animation: –is-active–example1; animation-timeline: var(–is); –is-active: ; outline: var(–is-active, 4px solid

hotpink
PERSON

); } .example1 [style*=’–for:’]:is(:hover, :focus-visible) { view-timeline: var(–for); } .example1 { timeline-scope:

–property
PERSON

, –value, –none, –display, –hidden, –visibility,

–zero
PERSON

,

–opacity
ORG

; }

It is a bit more involved than the anchor-positioning one, so let’s go through it step by step.

N amed

Timeline Range Keyframe
ORG

Selectors

Let’s start from the @keyframesGo to a sidenote :

Side note: It is not necessary to use a dashed ident here; any custom ident would work, but I recently prefer to use dashed idents for any custom entities in

CSS
ORG

, as I find them much easier to differentiate from regular ones. Jump to this sidenote’s context.

@keyframes –is-active–example1 { entry

0%
PERCENT

, exit

100%
PERCENT

{ –is-active: initial; } }

This was the

first
ORDINAL

time I played with

Named Timeline Range Keyframe Selectors
ORG

.

They are a handy way of specifying the timeline range right in the @keyframes , which gives us the benefit of no longer having to write the explicitGo to a sidenote animation-range: entry

0% exit 100%
PERCENT

whenever we use these keyframes, making it easier to reuse them.

Side note:

One
CARDINAL

alternative to using this new @keyframes feature I looked into was hacking the explicit range to be just animation-range:

0 0
CARDINAL

, and then setting the animation-fill-mode: both to make the animation apply all the time, but that is more cumbersome and would require wrapping the animation with @supports to mask it from browsers that do not support scroll-driven animations. By embedding the range right into the keyframes, we avoid all of these issues. Jump to this sidenote’s context.

The entry

0%
PERCENT

, exit

100%
PERCENT

value covers the whole distance the element can be in inside its scrollport.

The main difference from the regular animation-range : we can only use percentages here, so we can’t use values in px , calc() etc.

The declaration that we set is –is-active: initial , which might seem weird, but it is just the “space toggle” in action. A bit more on this later.

A pplying the Animation and Using the State

.example1 [

style*=’–is
PERSON

:’] { animation: –is-active–example1; /*

1
CARDINAL

*/ animation-timeline: var(–is); /*

2
CARDINAL

*/ –is-active: ; /*

3
CARDINAL

*/ outline: var(–is-active, 4px solid

hotpink
PERSON

); /*

4
CARDINAL

*/ }

We’re using the animation shorthand to mention these @keyframes we defined previously. It doesn’t matter if we use the shorthand or just an animation-name here. The important part is applying the timeline from the –is CSS variable. Here we are basically “subscribing” to a particular named timeline. More on this later. The initial state for our –is-active space Go to a sidenote toggle Using the space toggle: by default the outline would be just an empty value, but as soon as the animation would be applied, the –is-active would become initial , and the fallback value ( 4px solid

hotpink
PERSON

) would be used.

D elivering the State

.example1 [style*=’–for:’]:is(:hover, :focus-visible) { view-timeline: var(–for); }

Whenever we want to apply our state — in this case, when we hover or focus over our element with the –for variable defined inline — we can apply this variable as view-timeline , and that’s it! Oh, wait, no, it isn’t. We forgot the most important part:

L ifting the State Up with

the Timeline Scope
WORK_OF_ART

.example1 { timeline-scope:

–property
PERSON

, –value, –none, –display, –hidden, –visibility,

–zero
PERSON

,

–opacity
ORG

; }

In order for this technique to work, we have to explicitly define the scope within which the timelines could be used. This is the biggest limitation of this method, as we have to list all the values we’d be using for our timelines; otherwise, we couldn’t define and reuse them on completely different elements in our scope’s subtree.

Because this requires only modifying

one
CARDINAL

value of

one
CARDINAL

CSS propertyGo to a sidenote, this can be done inline in HTML, so it does not require creating new rules in the stylesheets.

Side note: If we’d like, we could also utilize CSS variables, so multiple places could contribute to the same property (but it might be tricky to deliver these, as we could only do so from any ancestor elements, so I’m not sure about the use cases for this). Jump to this sidenote’s context.

There is good news: there is a chance we would get an all keyword possible for the timeline-scope property, which would allow us to just get everything and not care about listing all the values explicitly. You can subscribe to this

CSSWG
ORG

issue if you’d like to follow any developments of this feature. When we have this built-in, this technique will become so much more powerful.

Now, that’s really it. For the basic technique.

V ariations

O ne to Many

With anchor positioning, we initially had a limitationGo to a sidenote where we couldn’t apply multiple anchor names to a single element. With timelines, we don’t have this problem, so it was very easy for me to modify our example above to allow a single element to target multiple others:

Side note: Since my

first
ORDINAL

experiments, I did open an issue about it, which was resolved with the latest version of

Chrome Canary
ORG

supporting multiple anchor names! Jump to this sidenote’s context.

Which property corresponds with which value? display visibility opacity hidden

0
CARDINAL

none

Toggle to a videolive example Hovering or focusing on various items in this example would highlight multiple other elements.

Here we didn’t touch the

CSS
ORG

and only modified the HTML, re-shuffling our idents so

one
CARDINAL

element now contains multiple names in a –for variable:

<em style=" –is:

–property
PERSON

; –for: –display, –visibility,

–opacity
ORG

; ">

It works the same: the –for variable is delivered to the view-timeline property, which would happily accept any number of comma-separated timeline names; we don’t need to do anything special in addition to this.

M any to

One
CARDINAL

Ok, so we can pass multiple values to the –for , but what about the –is ? It won’t work as we would expect it to. Here is a broken example:

What is a property which has hidden as a possible value? What is a value which can be assigned to a display property? none !important visibility

Toggle to a videolive example Hovering or focusing on the

first
ORDINAL

term highlights the proper items, but doing so on the next term does not work.

We kept the CSS the same, but the HTML for the above example contains this for the list items:

<li> <code style="–is:

–property
PERSON

, –none"> visibility </code> </li> <li> <code style="–is: –none, –value, –display"> none </code> </li>

We can see that when we pass multiple comma-separated values to the –is , only the

first
ORDINAL

one works, making it so one using

–property
PERSON

works but nothing else does.

Why is that? Can we fix it? We can!

What is a property which has hidden as a possible value? What is a value which can be assigned to a display property? none !important visibility

Toggle to a videolive example Hovering or focusing on all the terms would properly highlight their targets.

The fix is not perfect and can look a bit weird:

.example1-fixed [

style*=’–is
PERSON

:’] { animation-name: –is-active–example1, –is-active–example1, –is-active–example1; }

Yes, we did repeat the same animation-nameGo to a sidenote

three
CARDINAL

times. Unlike other animation sub-properties, the animation-name is never repeated by itself.

Side note: As I’m using this to override the existing styles, I’m not using a shorthand, as otherwise we would lose the animation-timeline value, but if we wanted to define this right away, we could still use the shorthand. Jump to this sidenote’s context.

When we provide multiple comma-separated values to animation-timeline , it does not create new animations. We can think of animation-name as the leading sub-property; all others are followers. If we have

only one
CARDINAL

name,

only one
CARDINAL

animation is applied. So we cannot apply any of the values animation-timeline which go to the non-existent animations. But if we define the name

three
CARDINAL

times, we could “enable” each of the “slots” with our technique.

It’s not very convenient, but it works.


S ingle Connecting Property

ORG

Before, we did use

two
CARDINAL

different properties: –is and –for to connect our elements. This is just one of the many ways we could implement this; different needs might require different methods.

One
CARDINAL

other way we could do it is to use a single property, especially when we want the connection to go both directions, like with the sidenotes in my blog.

If we don’t want to have groups of elements, we can simplify the

first
ORDINAL

example by using

only one
CARDINAL

custom property:

display visibility opacity hidden

0
CARDINAL

none

Toggle to a videolive example Hovering or focusing on any of the terms connects it with another one in the pair.

Here is the complete CSS responsible for this

second
ORDINAL

example:

@keyframes –is-active–example2 { entry

0%
PERCENT

, exit

100%
PERCENT

{ –is-active: initial; } } .example2 [style*=’–property:’] { animation: –is-active–example2; animation-timeline: var(–property); –is-active: ; outline: var(–is-active, 4px solid

hotpink
PERSON

); } .example2 [style*=’–property:’]:is(:hover, :focus-visible) { view-timeline: var(–property); } .example2 { timeline-scope: –display, –visibility,

–opacity
ORG

; }

If I wanted to replicate the

first
ORDINAL

example more closely, I could have added : not (: hover, : focus-visible) to the rule with the animation, but I found the behavior where we highlight both elements each time even more useful.

And the only changes are a shorter list for timeline-scope and that the same variable is used for the selector and variable name.

B oolean Logic

As with any other space toggles, we can apply a limited subset of boolean logic to them, like doing NOT, but for the “not active” state, I prefer to add a

second
ORDINAL

space toggle, as it makes things easier to use. For example, the sidenotes on this page use these styles:

@keyframes –is-active { entry

0%
PERCENT

, exit

100%
PERCENT

{ –is-active: initial; –not-active: ; } } [style*=’–sidenote:’]:not(:hover, :focus-within) { animation: –is-active; animation-timeline: var(–sidenote); –is-active: ; –not-active: initial; } [style*=’–sidenote:’]:is(:hover, :focus-within) { view-timeline: var(–sidenote); } .Sidenote::before { opacity: var(–is-active,

1
CARDINAL

) var(–not-active,

0
CARDINAL

); } .Sidenote::after, .Sidelink::after { background: var(–is-active, var(–LIGHT, rgba(255,

255
CARDINAL

,

0
CARDINAL

,

0.3
DATE

))

var(–DARK
ORG

,

rgba(150
NORP

,

140
CARDINAL

,

90
DATE

,

0.3
DATE

)) ) var(–not-active, transparent); }

Having

two
CARDINAL

variables each time: –is-active and –not-active is much more convenient than having to define a separate temporary variable if we’d want to use the NOT condition.

We can see how it is very easy to nest the space toggle values: for the active state, we can apply different values for the light and dark themes, as they’re also implemented with space toggles!

Note that we could have still omitted the –not-active for the background , but I like to make things more explicit when possible.

T ransitions

If you did manage to play with the sidenotes on this post, you could notice the transitions they have.

Here is a video of how they work:

Sorry, your browser doesn’t support embedded videos, but don’t worry, you can download it. A video of

one
CARDINAL

of this article’s sidenotes, showing how hovering over the sidenote highlights its reference with a transition, and the other way around: hovering over the reference highlights the corresponding sidenote.

An interesting aspect of the properties set by animations is that we cannot use them for transitions on the same element due to the animation tainting.

However, what we can do is have transitions on the children of the elements with our animations. By using pseudo-elements, I’m able to toggle the background and opacity with a transition.


One
CARDINAL

important note I’d want to add is that this is more experimental than the scroll-driven animations themselves; I did test this behavior without them, and while it currently works in both

Chrome
ORG

and

Safari
ORG

, it does not work in

Firefox
ORG

yet. There is a known issue with custom properties toggled by animations, but in my testing, there is a difference even for regular inherited properties as well. I did open an issue in

Mozilla
ORG

’s bugzilla about that.

M ultiple States

In the previous examples, we had

only one
CARDINAL

state: combined hover and focus. But what if we’d like to have them separately and have

three
CARDINAL

or more different states?

Here is the

first
ORDINAL

example, but with added differentiation of the focus and hover states:

Which property corresponds with which value? display visibility opacity hidden

0
CARDINAL

none

Toggle to a videolive example In this example, there is a difference between hovering and focusing the terms.

There might be different ways this can be implemented. The one I choose for this example is not ideal, but it is the least intrusive: we have to add

only one
CARDINAL

an additional timeline to the scope, then define the keyframes for it, using

two
CARDINAL

states for on/off values:

@keyframes –is-focused { entry

0%
PERCENT

, exit

100%
PERCENT

{ –is-focused: initial; –not-focused: ; } }

And then, when using it, use nested space toggles:

.example3 [

style*=’–is
PERSON

:’] { animation: –is-active–example3, –is-focused; animation-timeline: var(–is), –is-focused; –is-active: ; –is-focused: ; –not-focused: initial; outline: var(–is-active, var(–not-focused, 2px solid pink) var(–is-focused, 4px solid

hotpink
PERSON

) ); }

By doing this, we always know which element is currently hovered and focused and can differentiate which state it is based on the additional timeline we flip.

The downside of this method is that whenever we focus any of the items and then use hover without removing the focus

first
ORDINAL

, our hover styles would be the same as focused, as the focus state timeline is universal.


One
CARDINAL

way to handle this would be to introduce

two
CARDINAL

different timelines per value, which is a bit cumbersome, or to introduce helpers for every item and do some clever stuff with view timelines, where we could always have the same timeline but would modify it in a way that would “choose” the right position corresponding to the keyframe we want to use. I’ve already been working on this article for too longGo to a sidenote. If you want, you can treat this as your homework: go and play with this technique and try to improve it!

Side note: Originally, this should have been a much smaller article. I understand that the technique is a bit niche, but I hope there were enough interesting bits here and there that reading it was worth it! Jump to this sidenote’s context.

F inal Words and Credits

That technique comes from my previous attempts at implementing it via anchor positioning, alongside a few other articles that were not directly related but still did contribute some inspiration and motivation:


Eric Meyer
PERSON

’s “Nuclear Anchored Sidenotes” article, with his take on using anchor positioning for sidenotes, in which he did call out my method of using anchor positioning for cross-highlighting the sidenotes (the newer one, from this CodePen).


Bramus’
WORK_OF_ART

“Solved by CSS Scroll-Driven Animations: Detect if an element can scroll or not” article, which used a variation of the effect I’m also using, where we can detect the scroll based on if the scroll timeline is applied. He also did use the space toggle, and I’m happy this technique (originally coined by

Jane Ori
PERSON

) gets more traction!


Johannes Odland
PERSON

’s “Scroll-persisted

State
ORG

” article. My article is about a different type of state, but I can see how these

two
CARDINAL

techniques could work in tandem to deliver a scroll-persisted state across a distance.

And, once again, even though I did use this technique for the sidenotes on my site, I do not recommend using scroll-driven animations for production. Only for these tiny progressive enhancement purposes, where you double-check that nothing would break in browsers that do not support the technology yet.

I can’t wait for timeline-scope: all to become available, as it would make this technique so much more powerful, and for scroll-driven animations to come to other browsers.