When it comes to CSS and HTML, application state falls into these categories:
cascade state: classes/checkbox state/other dom
pseudo state: based on current user interaction
animation state: pre-determined series, triggered by cascade and/or pseudo state
transition state: easing between the cascade and/or pseudo states when they change
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: ; }
}
Using this memory
animation sets the custom property --cell
as a Space Toggle which has two states, off 'initial' and on ' '.
You can concatenate CSS custom properties like so: If you concatenate a space: If you concatenate initial: 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 The TL;DR of Space Toggles
.demo {
--val1: 2px solid;
--val2: red;
--result: var(--val1) var(--val2);
}
--result
is effectively 2px solid red
.demo {
--toggle: ;
--value: green;
--result: var(--toggle) var(--value);
}
--result
is effectively just green
.demo {
--toggle: initial;
--value: green;
--result: var(--toggle) var(--value);
color: var(--result, cyan);
}
--result
is invalidated and var(--result, fallback) will use the fallback instead. (color is set to cyan in this example)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 */
}
Using the shorthand animation property, that looks more like this:
.demo {
animation: memory 1ms linear 1 forwards paused;
}
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);
}
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)
Do you think it's possibe to have:
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).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 toinitial
(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:
Thanks for explanation. I've noticed that this doesn't work in Chrome:
Inside this demo: codepen.io/propjockey/pen/b01b6646...
Does it only work with token
initial
and space?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!
So:
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.
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. 😉
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.)
Wow. Please no, but also, wow!
Yes 😈