DEV Community

Jane Ori
Jane Ori

Posted on

An Innovative Idea for Holding State in CSS

When it comes to CSS and HTML, application state falls into these categories:
cascade state: classes/checkbox state/other dom
screenshot of html and css producing a static state
pseudo state: based on current user interaction
gif showing HTML and CSS with a user interacting to trigger a temporary hover state
animation state: pre-determined series, triggered by cascade and/or pseudo state
gif showing a CSS animation triggered by clicking a checkbox
transition state: easing between the cascade and/or pseudo states when they change
gif showing CSS transition triggered by hover

These are the building blocks for all UX that can be combined in an infinite number of ways[1]. These are all intertwined of course, and we can split hairs on how I've referred to them, but ultimately though; Either our state is static - derived from the current DOM, or it's temporary - derived from the user's current interaction.

[1] except that animation state cannot be combined with animation state due to animation-tainting

"But wait, there's more!"

- @rockstarwind, probably

The new static state, stored in CSS (not DOM!)

Earlier this week, @rockstarwind posted an idea on their twitter that demonstrates a new technique for remembering temporary interactions without DOM needing to be the source of that memory (like it is with the :checked state of a checkbox).
They go on to show a reduced example in a codepen with excellent comments to learn from:

Neat!

Let's break down the idea and play with it a bit.

Register the one-bit memory cell

Following their work, this is what we have to do first:

@keyframes memory {
  0% { --cell: initial }
  1%, 100% { --cell: ; }
}
Enter fullscreen mode Exit fullscreen mode

Using this memory animation sets the custom property --cell as a Space Toggle which has two states, off 'initial' and on ' '.


The TL;DR of Space Toggles

You can concatenate CSS custom properties like so:

.demo {
--val1: 2px solid;
--val2: red;
--result: var(--val1) var(--val2);
}
Enter fullscreen mode Exit fullscreen mode

--result is effectively 2px solid red

If you concatenate a space:

.demo {
--toggle: ;
--value: green;
--result: var(--toggle) var(--value);
}
Enter fullscreen mode Exit fullscreen mode

--result is effectively just green

If you concatenate initial:

.demo {
--toggle: initial;
--value: green;
--result: var(--toggle) var(--value);
color: var(--result, cyan);
}
Enter fullscreen mode Exit fullscreen mode

--result is invalidated and var(--result, fallback) will use the fallback instead. (color is set to cyan in this example)

So in effect, a space toggle is is a one bit variable we can use to derive two completely different states from reading it with var() wherever and as often as we want.



Note: Animating custom properties is part of the Houdini spec, so this may not work in firefox for a while.

Initialize the memory

All we have to do here is apply the keyframes to an element:

.demo {
  animation-name: memory; /* keyframes reference */
  animation-duration: .1s;
  animation-delay: 0s;
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
  animation-timing-function: linear;
  animation-play-state: paused; /* must be paused at first */
}
Enter fullscreen mode Exit fullscreen mode

Using the shorthand animation property, that looks more like this:

.demo {
  animation: memory 1ms linear 1 forwards paused;
}
Enter fullscreen mode Exit fullscreen mode

Now .demo elements have a --cell property explicitly set to initial.

Set up properties to use the space toggle

.demo {
  --bg-if-cell-is-on: var(--cell) rebeccapurple;
  background: var(--bg-if-cell-is-on, hotpink);

  --color-on: var(--cell) white;
  color: var(--color-on, black);
}
Enter fullscreen mode Exit fullscreen mode

In English, if the cell is off, the background is hotpink and color is black. If the cell is on, the background is rebeccapurple and color is white.

Flipping the bit

Next, they use the :active pseudo state from a button to un-pause the animation. Theoretically we could use any pseudo state, like :hover.

Let's test

click 'rerun' in the bottom right corner if needed!

Great! If the user's pointer enters the document, CSS flips state and the background becomes rebeccapurple.

Un-flipping the bit

To return to the initial state, the easiest way is what they've shown, set animation: none; on our element when a selector matches.
This changes --cell back to initial implicitly because when a custom property is not set (nor registered with a specific syntax), it is initial.

The rest of the owl

We can use this technique to do something fun like 100% CSS etch-a-sketch:

Delightful!

"Remembering is so much more a psychotic activity than forgetting"

I've only been friends with RockStarwind for a couple years but they are continually sharing innovative ideas and awesome demos in the CSS space. Definitely give them a follow on Twitter so you don't have to remember to check manually. ;)

If you enjoyed my first article, please consider following me here and on twitter as well!

💜

Prior art!

After tweeting, I learned about someone already using this idea in a really really interesting way! Check it out!

and her demo:

HOW COOL IS THAT?!

Top comments (9)

Collapse
 
jcubic profile image
Jakub T. Jankiewicz • Edited

Do you think it's possibe to have:

--variabe: true;
--variabe: false;
Enter fullscreen mode Exit fullscreen mode

Somehow to toggle between two values using mutipe css variables and your technique?

I would be nice for libraries that use configuration to style the output.

So far for my jQuery Terminal I've used CSS boolean trick from artice: Conditions for CSS Variables. But it ony works with numbers. At least this is how I us it. I have --glow: 1 to enabe glow on text (using text shadow).

Collapse
 
janeori profile image
Jane Ori • Edited
.test {
--true: ;
--false: initial;
--variable: var(--false);

--red-if-true: var(--variable) red;
background: var(--red-if-true, blue);
}
.test:hover {
  --variable: var(--true);
}
Enter fullscreen mode Exit fullscreen mode

background of .test is blue unless you hover, it's red.

There is one major downside to this though, and it's complicated.
If you have .test nested inside another .test element and --red-if-true computes to initial (false) on the child, it will inherit first instead of using the fallback immediately in a few browsers.
The spec used to say inheriting was correct but they fixed it so the fallback will be used instead of inheriting as part of the Houdini spec when I pointed out the problem. Chrome will use fallback for all but a few versions from about a year ago. Firefox used to use fallback but they won't again until they implement Houdini. Safari inherits.
It's extremely unfortunate.

The fix is to use an intermediate reset layer that explicitly sets the computed variable to initial so when inheriting behavior is present, it inherits initial and the fallback is used even if the outer-most .test is truthy:

<span class="test">
  <span style="--red-if-true: initial;">
    <span class="test"></span>
  </span>
</span>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jcubic profile image
Jakub T. Jankiewicz • Edited

Thanks for explanation. I've noticed that this doesn't work in Chrome:

html:hover {
  --enable: paused;
  animation-play-state: var(--enable) running;
}
Enter fullscreen mode Exit fullscreen mode

Inside this demo: codepen.io/propjockey/pen/b01b6646...

Does it only work with token initial and space?

Thread Thread
 
janeori profile image
Jane Ori

Yes, the space toggle is only initial and space. The tl;dr on space toggles in the article should help explain what's happening. LMK if you want more clarity on something, happy to help!

Thread Thread
 
jcubic profile image
Jakub T. Jankiewicz • Edited

So:

--var: initial;
Enter fullscreen mode Exit fullscreen mode

Just reset CSS variable to initial value which is no value at all. Am I right? If so I would write this in TL;DR section. I wonder how it would behave if you register the CSS variable in JavaScript with initial value.

Collapse
 
garrettl profile image
Garrett LeSage

This is a definitely neat and experimentation is great.

However, it should have a huge warning at the top to not even consider this in production sites for a while yet. It simply does not work in Firefox or Safari, with no ETA.... and even in the future where it might, it would break things for a while in older browser engines (such as enterprise versions, like Firefox Extended Support Release or various Apple products that are out of support for an OS upgrade, and therefore will not get a WebKit browser engine upgrade).

There are alternate, cross-browser, standard-based methods to perform these kinds of demos that should be preferred. There's no need to break websites and web apps for non-Chromium browsers.

(Even high profile development sites like web.dev are sometimes guilty of promoting Chromium-only solutions without disclaimers. But, of course, web.dev is built by Google, so they're biased...)

Thanks for sharing though; it's certainly interesting! Hopefully nobody uses this in production quite yet. 😉

Collapse
 
garrettl profile image
Garrett LeSage

Just to quickly follow-up on the topic of incomplete browser support (but in a more general sense): There's a lot of useful stuff that's implemented in one or two browser engines that the other(s) do not pick up.

For an example that goes the other way: Firefox has had CSS subgrid for a few years now. Other browsers do not have this yet. You wouldn't make a layout depend on subgrid right now, no matter how much you might want to use it. (However, you could at least use subgrid as an enhancement as long as there's an adequate and graceful fallback for other browser... and hope that other browsers catch up.)

A lot of features can be used a bit early with progressive enhancement, provided we make sure it works with a fallback. Holding state is not one of these things though. It's just better to have state handled in a cross-browser way.

We must wait for browser engines to all catch up for any feature that's not fully implemented everywhere (and then give it a little bit of time after that) so that we don't break the web for our users. This isn't the era of Internet Explorer; we, as developers, shouldn't usher in another round of breaking the web for everything but one browser, even if that browser might happen to be the dominant browser. (We should not give the web over fully "on a silver platter" to one organization — especially to Google, who controls the development of Chromium, in this case.)

Collapse
 
ivanbozhkov profile image
Ivan Bozhkov

Wow. Please no, but also, wow!

Collapse
 
rockstarwind profile image
Rock Starwind

Yes 😈