Velin
Submit

Text effect

Stacked Line Reveal

by velin · Oct 8, 2026

Made from this prompt

Claude · Opus 5.5

A bold uppercase headline in two layers sharing one spot. Every line has its own overflow mask; on a loop the first layer slides up and out while the second rises from below, line by line with a 70 ms stagger and an alternating ±5% sideways offset. 0.9 s power4-out easing, a new face every 1.6 s. Plain HTML, CSS and JS.

Run it in your own Claude, or remix it right here. No account needed to copy.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stacked Line Reveal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@900&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #0e0d0c;
    --fg: #f3ede4;
    --accent: #ff5a36;
    --duration: 900ms;
    --stagger: 70ms;
    --ease: cubic-bezier(0.23, 1, 0.32, 1);
    /* Hidden lines sit a little past 100% so no glyph edge peeks under the mask. */
    --hidden: 120%;
  }
  * { box-sizing: border-box; margin: 0; }
  html, body { height: 100%; }
  body {
    display: grid;
    place-items: center;
    background: var(--bg);
    font-family: "Inter Tight", ui-sans-serif, system-ui, sans-serif;
    -webkit-font-smoothing: antialiased;
    overflow: hidden;
  }
  /* Two layers share one grid cell; each line has its own overflow mask. */
  .title { display: grid; place-items: center; text-align: center; }
  .layer { grid-area: 1 / 1; }
  .layer.base { z-index: 2; }
  .layer.alt { z-index: 1; }
  .mask { display: block; overflow: hidden; }
  .line {
    display: block;
    white-space: nowrap;
    font-size: clamp(2.5rem, 12vw, 9.5rem);
    font-weight: 900;
    text-transform: uppercase;
    line-height: 0.92;
    letter-spacing: -0.04em;
    will-change: transform;
    transition: transform var(--duration) var(--ease) var(--delay, 0ms);
  }
  .base .line { color: var(--fg); }
  .alt .line { color: var(--accent); }
  @media (prefers-reduced-motion: reduce) {
    .line { transition: none; }
  }
</style>
</head>
<body>
  <h1 class="title" aria-label="Years of your love brought us here. Now we walk one road together.">
    <span class="layer base" aria-hidden="true"></span>
    <span class="layer alt" aria-hidden="true"></span>
  </h1>
<script>
  const LINES = ["Years of", "your love", "brought us", "here"];
  const REVEAL = ["Now we", "walk one", "road", "together"];
  /** Time on each face: the 0.9 s transition plus line delays settle in about 1.2 s, then a short hold. */
  const INTERVAL_MS = 1600;
  const STAGGER_MS = 70;

  const build = (layer, words) =>
    words.map((text) => {
      const mask = document.createElement("span");
      mask.className = "mask";
      const line = document.createElement("span");
      line.className = "line";
      line.textContent = text;
      mask.append(line);
      layer.append(mask);
      return line;
    });

  const base = build(document.querySelector(".base"), LINES);
  const alt = build(document.querySelector(".alt"), REVEAL);

  function render(state) {
    base.forEach((el, i) => {
      // Out from the top line first, back from the bottom line first, so the wave flows one way.
      const delay = state === "enter" ? i + 2 : state === "revealed" ? i : base.length - 1 - i;
      el.style.setProperty("--delay", `${delay * STAGGER_MS}ms`);
      el.style.transform =
        state === "start" ? "translate3d(0, var(--hidden), 0)"
        : state === "revealed" ? "translate3d(0, calc(var(--hidden) * -1), 0)"
        : "translate3d(0, 0, 0)";
    });
    alt.forEach((el, i) => {
      // Alternate ±5% sideways so the lines slide in at a slant.
      const x = i % 2 === 0 ? 5 : -5;
      const delay = state === "revealed" ? i + 1 : alt.length - 1 - i;
      el.style.setProperty("--delay", `${delay * STAGGER_MS}ms`);
      el.style.transform = state === "revealed" ? "translate3d(0, 0, 0)" : `translate3d(${x}%, var(--hidden), 0)`;
    });
  }

  render("start");
  // Two frames so the start position is painted before the entrance transition runs.
  requestAnimationFrame(() => requestAnimationFrame(() => {
    render("enter");
    let revealed = false;
    setInterval(() => {
      revealed = !revealed;
      render(revealed ? "revealed" : "back");
    }, INTERVAL_MS);
  }));
</script>
</body>
</html>

No remixes yet. Fork it and be the first.