DEV Community

Cover image for The CSS SMEAR hack
Jane Ori
Jane Ori

Posted on

The CSS SMEAR hack

The SMEAR technique enables infinite generations of simulations, games, or other CSS experiments to happen without any JS and without revealing the sauce to the User.

gif of SMEAR being used

SMEAR stands for Shrouded Markup Editing And Regeneration - a description of what happens to advance the application state to the next generation without JS. The smearing gesture itself has to be more deliberate and slower than a swipe, and is where the name SMEAR actually comes from. Forcing SMEAR into an acronym happened after the fact as a terrible joke.

The SMEAR hack is somewhat involved in the details, so to navigate that complexity, let's walk backwards from the checkers demo and reveal the work one layer at a time.

Image of an onion being peeled

!But first! Go hands-on with the SMEAR if you'd like (Chrome and Safari - desktop only) by selecting a front-row checker, then select a direction to move it in, then SMEAR to confirm:

Shrouded? What is the SMEAR hack doing?

The big reveal is right here in our first layer:

SMEAR is a drag-drop.

Code (CSS in this case) is on the right, and the user drags it into a contenteditable element (Style tag display block, contenteditable plaintext-only in this case) on the left.

We make the CSS and the style tag invisible to the user and put a fake call-to-action pseudo element in place for the user to seemingly-but-not-really interact with.

The last key at this layer is that the element with our code (shrouded markup) has user-select: all; so any interaction selects all of the code automatically and the drag action begins immediately.

Here's an unshrouded bare bones demo of what's happening:

user-select: almost-all; How to change the code each generation

In order for the patch to be regenerated from our current state, we have to include markup that can programmatically represent any possible patch.

If we could somehow allow users to select pseudo element content, it would be much easier to generate the next generation's state from CSS. As it is now though, we have to omit the elements that don't apply to the next generation from a superset.

There are a few ways to omit text from the user-select: all;

user-select: none; is surprisingly not one of them because it still copies the seemingly unselected text. Probably a bug.

display: none; works but if your patch state is derived from an animation, you cannot set display to none. (Spoiler: we need to change our generated code from an animation hack)

visibility: hidden; works and can be set from an animation. Yay!

The state of our patched code will come down to math - like showing the win screen when the opposite player's total sum/count of pieces on the board is 0. So we need to cast an integer from a calc() to visibility: hidden;, which requires an animation.

@keyframes markupToggler { from { visibility: hidden; } }


animation: markupToggler 1ms linear both var(--condition, 0);

If --condition is 0, animation applies the from state, if --condition is 1, the animation stops applying the from state.

Here's what that looks like in action: note the content of our CSS patch regenerates every time we SMEAR and shows the opposite state.

This animation can be reused on any element, just set the --condition variable to a calc (or min/max/clamp/etc) that becomes 0 or 1 and you're good to go.

Forcing new code to the bottom of the style element

There are a number of ways to prevent the user from dragging code into the middle of existing code (something which you may have done while interacting with the previous demo?) but the most reliable I've used (which could be trimmed down or done differently) is just adding this:

style.shrouded-markup::before {
  content: "";
  display: block;
  margin-bottom: -999999vh;
style.shrouded-markup::after {
  content: "\A\A\A\A";
  display: block;
Enter fullscreen mode Exit fullscreen mode

Shrouding Reality

The only important gotcha here is that you'll want your fake call-to-action element to be on the ::before pseudo specifically, not ::after.

What's next?

Maybe a follow up article if there's interest!

  • How to user-select: all; on contenteditable elements (you can!) and
  • using a textarea and reset button (JS not required) to empty cache.

The End!

If you think this idea/trick is neat, it's the kind of thing I do all the time! So please do consider following me here and on twitter as well!

// Jane Ori

Top comments (1)

afif profile image
Temani Afif

Yous should definitely send your articles to CSS Tricks 😉