Back to the library

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.

textemphasis

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>