slop: motion

Motion is information or it is removed. Each section puts the common animated default (A) next to the still or composited version (B) with the same content, then states what the animation costs. Patterns are collected from impeccable.style/slop; the reasoning, demos and samples are ours. Hover the demos: they are live.

pulsing dot

The slop: every status dot on the page pulses forever. The rule: a dot is a state, not an alarm; color carries the state and motion marks only the moment the state changes.

An animation that never stops has no off position, so the reader cannot tell "live" from "decorated". When everything pulses, a genuinely failing service has nothing left to escalate with.

A: everything pulses

  • apioperational
  • workeroperational
  • edgeoperational

three healthy services demand attention every two seconds; the animation carries no information.

B: color carries the state

  • apioperational
  • workeroperational
  • edgeoperational

same rows, still. the reader scans color, not motion; a ping plays once, on the transition.

ruling
Reserve motion for state changes: a dot pings once when it turns red, never in its steady state.
B markup + rules
<span class="dot is-ok"></span>
.dot { size-2; rounded-full }
.dot.is-ok { bg-emerald-400 }
.dot.is-degraded { bg-amber-400 }
/* motion only on the change itself */
.dot.just-changed { animation: ping 600ms ease-out 1 }

blinking cursor

The slop: a fake terminal hero whose caret blinks forever on a marketing page. The rule: a caret blinks to mark focus in an input the reader can type into; in a display the reader cannot type into, it is drawn solid or not at all.

The blink borrows the one signal a terminal earns from real input and spends it as decoration, so the eye keeps checking a cursor that will never accept typing.

A: blinking forever

~ deploy $ vercel --prod
▌

the caret pulses with no input behind it; the page flickers at the reader.

B: solid caret

~ deploy $ vercel --prod
▌

same line, no animation. the block reads as a caret without promising focus.

ruling
A caret blinks only where the reader can type. Elsewhere it is a solid block or removed.
B markup + rules
<pre class="term">~ deploy $ vercel --prod<span class="caret"></span></pre>
.caret { inline-block; w-2; h-[1em]; bg-zinc-500 }
/* no animation: a display the reader
cannot type into has no focus to show */

marquee

The slop: logos or quotes drifting across the page in an infinite loop. The rule: if the content is worth showing, it is worth reading; motion never competes with reading itself.

A marquee makes the reader chase its content: anything past the fold has already moved, and stopping to read one name means watching the rest queue. The loop also burns a composited layer forever to say what a static row says for free.

A: the loop

acmeboltanimbuskorivelapico
acmeboltanimbuskorivelapico

the same six names, duplicated and scrolling; read one and you lose the rest.

B: still and readable

acmeboltanimbuskorivelapico

same six names, wrapped once; the reader reads at their own pace.

ruling
Logos and quotes sit still. If the row does not fit, wrap it or paginate it.
B markup + rules
<div class="logo-row">
<span>acme</span><span>bolta</span> ...
</div>
.logo-row { flex; flex-wrap; items-center;
gap-x-8; gap-y-3 }
/* the loop is gone: the reader reads
at their own pace */

bounce easing

The slop: an overshoot curve (spring, elastic, back) on interface elements, stretched over half a second. The rule: easing starts fast and ends quiet; interface elements are not balls.

The overshoot says "physical object settling", but nothing on a screen has weight, so the reader sees a wobble with no meaning. Long spring durations also queue: hover away mid-bounce and the element keeps traveling after the pointer has left.

A: spring and wobble

700ms overshoot on hover: playful once, exhausting on every element, every time.

B: fast in, quiet out

same button, 150ms ease-out on a color shift; the state change is confirmed, not performed.

ruling
150ms ease-out for state feedback. No overshoot curves, no durations past 300ms on hover.
B markup + rules
<button class="btn">deploy</button>
.btn { transition: background-color 150ms ease-out }
.btn:hover { background: indigo-500 }
/* transform feedback, if wanted: scale(1.02),
same 150ms ease-out. no overshoot curve,
nothing past 300ms */

layout animation

The slop: animating width, height or margins, so the browser reflows the page on every frame. The rule: animate only transform and opacity; they composite on the GPU while layout properties reflow everything after them.

The cost is not only jank: a height animation pushes the whole page below it every frame, so the reader's eye loses its anchor mid-motion. The reveal lands as an overlay instead, and nothing the reader was reading moves.

A: the page reflows

deploy log
step 1 · build passed
step 2 · migrated
row below: pushed while the height animates

hover: height interpolates and the row below jumps for 300ms; every frame is a layout pass.

B: transform and opacity

deploy log
step 1 · build passed
step 2 · migrated
row below: never moves

same content as an overlay; the row below never moves and the animation composites.

ruling
Animate transform and opacity only. If a reveal would push content, it becomes an overlay.
B markup + rules
<div class="row">deploy log</div>
<div class="panel">step 1 · step 2</div>
<div class="below">row below: never moves</div>
.panel { position-absolute; opacity-0; -translate-y-1;
transition: opacity 150ms ease-out,
transform 150ms ease-out }
.row:hover + .panel { opacity-1; translate-y-0 }
/* never animate width/height/margin: they
reflow the page every frame */

hover zoom

The slop: a card whose cover zooms to 1.5x over 700ms while the whole card scales too, often without a mask, so the image bleeds past its own radius. The rule: a hover state confirms; it does not perform.

The zoom destroys the image the card exists to show: at 1.5x the cover is a crop, the text next to it loses its neighbor, and the unmasked edges turn a contained component into spillover. A 3% drift inside a mask keeps the "this responds" signal at a fraction of the cost.

A: zoom and bleed

sunset over the lake

shot at golden hour from the east pier.

hover: the card scales, the cover crops to 1.5x and escapes its own radius.

B: masked and modest

sunset over the lake

shot at golden hour from the east pier.

same card: the cover drifts 3% inside its mask over 200ms; the card itself stays put.

ruling
The image breathes a few percent inside its own mask; the card never scales, and nothing crosses its radius.
B markup + rules
<figure class="card">
<div class="mask"><div class="cover"></div></div>
<figcaption class="body">...</figcaption>
</figure>
.card { overflow-hidden; rounded-xl }
.mask { overflow-hidden } <!-- the zoom stays inside -->
.cover { transition: transform 200ms ease-out }
.card:hover .cover { transform: scale(1.03) }
/* the card itself never scales */

hidden until animated

The slop: content rendered at opacity 0, waiting for a staggered entrance to promote it to visible. The rule: content is readable at first paint; an entrance may move what is already visible, briefly, and never decides when the reader may start reading.

The entrance taxes the reader's first seconds: the largest text on the page is invisible while the animation plays, and if the script that triggers it fails or the reader is fast, the content is a blank region. Motion that gates content converts decoration into latency.

A: the stagger

alpha: one line of content, held back
beta: one line of content, held back
gamma: one line of content, held back

three cards fade up over 1.3s; the reader waits for permission to read.

B: present at first paint

alpha: one line of content, readable now
beta: one line of content, readable now
gamma: one line of content, readable now

same three cards, visible immediately; nothing stands between the reader and the text.

ruling
Render visible. If an entrance is wanted at all, it is a 150ms transform on content that is already there.
B markup + rules
<ul class="list">
<li>alpha</li><li>beta</li><li>gamma</li>
</ul>
/* visible at first paint; no opacity gate,
no script deciding when the reader may read.
an entrance, if wanted, moves what is
already there: */
@keyframes arrive { from { transform: translateY(8px) } }
.list li { animation: arrive 150ms ease-out 1 }

search pages

go to any page