Emphasis — the rest recedes
Variant: Context recedes
Emphasis by subtraction: nothing is added to the phrase, the prose around it falls from 78% to 26% and the term simply stops being dimmed with everything else. Because the term never changes, there is no frame in which the reader has to find it again — which is the whole reason to reach for this one in a product tour or a diff, where a highlight would be a second thing to look at. The dim costs 198ms and the restore 378ms: attention should land the moment it is called for, and a release as fast as the call reads as the page flickering. One property animates on one element, since colour inherits and the term only has to opt out.
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Emphasis — the rest recedes</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — 4.4s, shared across the emphasis family. The mark begins at
14% in every sibling and is fully released by 88%, so the four can be
compared frame for frame. */
--cycle: 4.4s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
/* Scratch colours: the two states the surrounding prose travels
between. 78% is ordinary body copy, not full slate — the term is
already a shade ahead of its context before anything animates. */
--_read: color-mix(in srgb, var(--slate) 78%, transparent);
--_recede: color-mix(in srgb, var(--slate) 26%, transparent);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(12px, 3vw, 28px);
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
}
/* The animation lives on the container and moves one property: the
inherited colour. Every descendant that has not claimed a colour of its
own travels with it, which is why the term only has to opt out. */
.doc {
width: min(360px, 92vw);
padding: clamp(14px, 3.4vw, 22px) clamp(15px, 3.6vw, 24px);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
background: var(--paper);
color: var(--_read);
animation: recede var(--t) infinite;
}
/* Opacity rather than a colour of its own — a second colour would drop
out of the inheritance chain and stop receding with the rest. */
.eyebrow {
font-size: clamp(11.5px, 2.5vw, 13px);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
.copy {
margin-top: clamp(7px, 1.4vw, 11px);
display: grid;
justify-items: start;
}
/* Hard-broken and nowrap, so all four siblings break in the same places
at every stage width and the term always opens the second line. */
.line {
white-space: nowrap;
font-size: clamp(13px, 3.1vw, 18px);
line-height: 1.5;
letter-spacing: -0.005em;
}
.term {
color: var(--slate);
font-weight: 500;
}
/* 198ms out, 378ms back. Attention should land the moment it is called
for; letting go of it is not urgent, and a restore as fast as the dim
reads as the page flickering rather than as the reader being released.
Both halves are ease-out — the surround is responding, and responses
decelerate. */
@keyframes recede {
0%,
14% {
color: var(--_read);
animation-timing-function: var(--ease-out);
}
18.5%,
78% {
color: var(--_recede);
animation-timing-function: var(--ease-out);
}
86.6%,
100% {
color: var(--_read);
}
}
@media (prefers-reduced-motion: reduce) {
/* Nothing here moves in the first place — the whole piece is a colour
change, and colour that carries meaning survives reduced motion. The
cycle stretches 1.7x and the recede stops short of the full depth, so
the contrast swing is gentler without the emphasis going away. */
.doc {
animation-duration: calc(var(--t) * 1.7);
animation-name: recede-calm;
}
@keyframes recede-calm {
0%,
14% {
color: var(--_read);
animation-timing-function: ease;
}
22%,
78% {
color: color-mix(in srgb, var(--slate) 42%, transparent);
animation-timing-function: ease;
}
90%,
100% {
color: var(--_read);
}
}
}
</style>
</head>
<body>
<div class="doc">
<p class="eyebrow">Planner notes</p>
<div class="copy">
<p class="line">Rows come back in index order,</p>
<p class="line"><span class="term">not heap order</span>, so the seek</p>
<p class="line">still pays for every fetch.</p>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.