Back to the library

Transcript — interlinear gloss

Variant: Interlinear gloss

A Leipzig-style interlinear gloss assembling itself: each gloss drops 0.4em out of its own word on an ease-out, staggered 60ms in reading order — origin-aware, the way a popover grows from its trigger — then the = and - boundaries light in both tiers at once. That beat is the argument: the rules require source and gloss to carry the same boundaries, so lighting them together proves the annotation is aligned, not merely underneath. The free translation wipes in last with a clip-path reveal, the clip being an entrance, and the annotations leave in the same reading order, each in 180ms, while the source line stays put, because the text is the thing and the gloss is a layer over it. Word and gloss share a grid column, so alignment holds at any width. Use it for tap-to-translate, annotated code or prose, and any reveal where a second tier must visibly belong to the first.

textstaggerclip-pathloop

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>Transcript — interlinear gloss</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 — 6s: glosses drop in at 10-15.7%, the boundaries light at
           24% and let go at 40%, the free translation wipes in at 46-52%, and
           every annotation leaves together at 86%. */
        --cycle: 6s;
        --t: calc(var(--cycle) / var(--speed));

        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);

        --_ink: color-mix(in srgb, var(--slate) 88%, transparent);
        --_gloss: color-mix(in srgb, var(--slate) 62%, transparent);
        --_muted: color-mix(in srgb, var(--slate) 55%, transparent);
        --_drop: -0.4em;
      }

      * {
        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);
        color: var(--slate);
        font-family:
          system-ui,
          -apple-system,
          'Segoe UI',
          Roboto,
          sans-serif;
      }

      .card {
        width: min(340px, 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);
      }

      .eyebrow {
        font-size: clamp(11px, 2.4vw, 12.5px);
        font-weight: 600;
        letter-spacing: 0.09em;
        text-transform: uppercase;
        color: var(--_muted);
      }

      /* Each word and its gloss share a column, so the tiers align by
         construction — the column is as wide as whichever line is longer. */
      .igt {
        margin-top: clamp(9px, 2vw, 13px);
        display: flex;
        gap: 0 1.1em;
        font-size: clamp(16px, 4.4vw, 21px);
        line-height: 1.45;
      }

      .col {
        display: grid;
      }

      .src {
        font-style: italic;
        color: var(--_ink);
      }

      /* Glosses emerge downward out of their own word — origin-aware, the
         way a popover grows from its trigger — staggered 60ms left to right
         in reading order. */
      .gl {
        font-size: 0.84em;
        color: var(--_gloss);
        opacity: 0;
        animation: gloss var(--t) infinite;
      }

      .col:nth-child(2) .gl {
        animation-delay: calc(0.06s / var(--speed));
      }
      .col:nth-child(3) .gl {
        animation-delay: calc(0.12s / var(--speed));
      }

      /* Grammatical labels in caps, Leipzig-style. Real capitals at a
         floor of 11px rather than synthesised small caps, which shrink to
         ~9px on a phone card. */
      .sc {
        text-transform: uppercase;
        font-size: max(11px, 0.78em);
        letter-spacing: 0.04em;
      }

      /* Morpheme boundaries: the Leipzig rule is that source and gloss carry
         the same number of = and -, so they light together across both tiers.
         That one beat is the diagram's argument — the gloss is aligned, not
         merely underneath. */
      .src b,
      .gl b {
        font-weight: inherit;
        animation: seam-mark var(--t) infinite;
      }

      .free {
        margin-top: clamp(8px, 1.8vw, 12px);
        font-size: clamp(13px, 3.5vw, 16.5px);
        color: var(--_ink);
        clip-path: inset(-0.3em 100% -0.3em 0);
        animation: wipe var(--t) infinite;
      }

      @keyframes gloss {
        0%,
        10% {
          opacity: 0;
          transform: translateY(var(--_drop));
          animation-timing-function: var(--ease-out);
        }
        13.7%,
        86% {
          opacity: 1;
          transform: translateY(0);
          animation-timing-function: var(--ease-out);
        }
        89%,
        100% {
          opacity: 0;
          transform: translateY(0);
        }
      }

      @keyframes seam-mark {
        0%,
        24% {
          color: inherit;
          animation-timing-function: ease;
        }
        27.5%,
        40% {
          color: var(--clay-d);
          animation-timing-function: ease;
        }
        44%,
        100% {
          color: inherit;
        }
      }

      /* A reveal is an entrance: ease-out, so the translation's first words
         are readable almost at once and the tail settles. */
      @keyframes wipe {
        0%,
        46% {
          clip-path: inset(-0.3em 100% -0.3em 0);
          opacity: 1;
          animation-timing-function: var(--ease-out);
        }
        52%,
        86% {
          clip-path: inset(-0.3em 0 -0.3em 0);
          opacity: 1;
          animation-timing-function: var(--ease-out);
        }
        89%,
        100% {
          clip-path: inset(-0.3em 0 -0.3em 0);
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* Annotations fade in place instead of dropping, and the translation
           keeps its wipe — a clip, not a journey. Everything slows. */
        :root {
          --_drop: 0;
        }

        .gl,
        .src b,
        .gl b,
        .free {
          animation-duration: calc(var(--t) * 1.7);
        }
      }
    </style>
  </head>
  <body>
    <div
      class="card"
      role="img"
      aria-label="An interlinear gloss of Japanese 'watashi=wa hon=o yon-da'. Glosses appear under each word: 1SG=TOP, book=ACC, read-PST. The morpheme boundaries light up in both lines, then the free translation 'I read the book.' wipes in."
    >
      <p class="eyebrow">Gloss · Japanese</p>
      <div class="igt">
        <div class="col">
          <span class="src">watashi<b>=</b>wa</span>
          <span class="gl"><span class="sc">1sg</span><b>=</b><span class="sc">top</span></span>
        </div>
        <div class="col">
          <span class="src">hon<b>=</b>o</span>
          <span class="gl">book<b>=</b><span class="sc">acc</span></span>
        </div>
        <div class="col">
          <span class="src">yon<b>-</b>da</span>
          <span class="gl">read<b>-</b><span class="sc">pst</span></span>
        </div>
      </div>
      <p class="free">‘I read the book.’</p>
    </div>
  </body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.