masked-reveal

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Masked Reveal

遮罩式渐显

Use When

适用场景

  • A headline or short text block needs a premium reveal on scroll.
  • Words should rise through an invisible mask with a staggered sequence.
  • The project already uses GSAP or needs ScrollTrigger-based motion.
  • 标题或短文本块需要在滚动时呈现高级感的渐显效果。
  • 文字需通过不可见遮罩按顺序逐字向上渐显。
  • 项目已使用GSAP,或需要基于ScrollTrigger的动效。

Motion Defaults

动效默认参数

  • Trigger: start when the text top reaches
    82%
    of the viewport.
  • Duration:
    0.7s
    to
    0.9s
    .
  • Stagger:
    0.025s
    to
    0.045s
    per word.
  • Offset:
    yPercent: 110
    to
    0
    .
  • Ease:
    power3.out
    or
    expo.out
    .
  • Replay: reveal once by default.
  • 触发条件:当文本顶部到达视口的82%位置时启动。
  • 时长:0.7秒至0.9秒。
  • 延迟间隔:每个文字0.025秒至0.045秒。
  • 偏移量:yPercent从110变为0。
  • 缓动效果:power3.out或expo.out。
  • 重播:默认仅触发一次渐显。

HTML

HTML

html
<h1 class="masked-reveal" data-masked-reveal>
  Design systems that feel alive from the first scroll.
</h1>
html
<h1 class="masked-reveal" data-masked-reveal>
  Design systems that feel alive from the first scroll.
</h1>

CSS Mask

CSS遮罩

css
.masked-reveal {
  visibility: visible;
}

html.js .masked-reveal[data-masked-reveal] {
  visibility: hidden;
}

html.js .masked-reveal.is-split {
  visibility: visible;
}

.masked-reveal .word-mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
}

.masked-reveal .word {
  display: inline-block;
  transform: translateY(110%);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  html.js .masked-reveal[data-masked-reveal] {
    visibility: visible;
  }

  .masked-reveal .word {
    transform: none;
  }
}
css
.masked-reveal {
  visibility: visible;
}

html.js .masked-reveal[data-masked-reveal] {
  visibility: hidden;
}

html.js .masked-reveal.is-split {
  visibility: visible;
}

.masked-reveal .word-mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
}

.masked-reveal .word {
  display: inline-block;
  transform: translateY(110%);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  html.js .masked-reveal[data-masked-reveal] {
    visibility: visible;
  }

  .masked-reveal .word {
    transform: none;
  }
}

GSAP ScrollTrigger

GSAP ScrollTrigger

This helper avoids the paid SplitText plugin and keeps spaces intact.
js
document.documentElement.classList.add("js");
gsap.registerPlugin(ScrollTrigger);

function escapeHTML(value) {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

function splitMaskedReveal(element) {
  if (element.dataset.maskedRevealReady === "true") return;

  const text = element.textContent.trim();
  element.setAttribute("aria-label", text);
  element.innerHTML = text
    .split(/(\s+)/)
    .map((part) => {
      if (!part.trim()) return part;
      return `<span class="word-mask" aria-hidden="true"><span class="word">${escapeHTML(part)}</span></span>`;
    })
    .join("");
  element.dataset.maskedRevealReady = "true";
  element.classList.add("is-split");
}

function initMaskedReveals(selector = "[data-masked-reveal]") {
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

  document.querySelectorAll(selector).forEach((element) => {
    splitMaskedReveal(element);
    const words = element.querySelectorAll(".word");

    gsap.set(element, { autoAlpha: 1 });
    gsap.fromTo(
      words,
      { yPercent: 110 },
      {
        yPercent: 0,
        duration: 0.8,
        ease: "power3.out",
        stagger: 0.035,
        scrollTrigger: {
          trigger: element,
          start: "top 82%",
          once: true,
        },
      }
    );
  });
}

initMaskedReveals();
这个工具类无需付费的SplitText插件,同时能完整保留空格。
js
document.documentElement.classList.add("js");
gsap.registerPlugin(ScrollTrigger);

function escapeHTML(value) {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

function splitMaskedReveal(element) {
  if (element.dataset.maskedRevealReady === "true") return;

  const text = element.textContent.trim();
  element.setAttribute("aria-label", text);
  element.innerHTML = text
    .split(/(\s+)/)
    .map((part) => {
      if (!part.trim()) return part;
      return `<span class="word-mask" aria-hidden="true"><span class="word">${escapeHTML(part)}</span></span>`;
    })
    .join("");
  element.dataset.maskedRevealReady = "true";
  element.classList.add("is-split");
}

function initMaskedReveals(selector = "[data-masked-reveal]") {
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

  document.querySelectorAll(selector).forEach((element) => {
    splitMaskedReveal(element);
    const words = element.querySelectorAll(".word");

    gsap.set(element, { autoAlpha: 1 });
    gsap.fromTo(
      words,
      { yPercent: 110 },
      {
        yPercent: 0,
        duration: 0.8,
        ease: "power3.out",
        stagger: 0.035,
        scrollTrigger: {
          trigger: element,
          start: "top 82%",
          once: true,
        },
      }
    );
  });
}

initMaskedReveals();

React Cleanup Pattern

React 清理模式

js
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    initMaskedReveals("[data-masked-reveal]");
  }, rootRef);

  return () => ctx.revert();
}, []);
js
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    initMaskedReveals("[data-masked-reveal]");
  }, rootRef);

  return () => ctx.revert();
}, []);

Taste Rules

风格准则

  • Use on short headlines, labels, and section intros; avoid long paragraphs.
  • Keep the vertical offset clean. Do not combine with blur unless the style explicitly calls for it.
  • Stagger by word, not letter, for a calmer editorial feel.
  • Initialize after fonts are loaded if line wrapping is critical.
  • Use
    ScrollTrigger.refresh()
    after late-loading images or layout shifts.
  • Do not split text that contains links, buttons, or meaningful inline markup.
  • 用于短标题、标签和章节引言;避免长段落。
  • 保持垂直偏移简洁。除非风格明确要求,否则不要结合模糊效果。
  • 按文字而非字母设置延迟间隔,营造更舒缓的编辑风格。
  • 如果换行至关重要,需在字体加载完成后再初始化。
  • 延迟加载图片或布局发生偏移后,调用
    ScrollTrigger.refresh()
  • 不要拆分包含链接、按钮或重要内联标记的文本。

Quick Checks

快速检查项

  • Text is hidden before GSAP initializes, then becomes visible with
    autoAlpha: 1
    .
  • Screen readers get the original full text through
    aria-label
    .
  • Spaces between words are preserved.
  • Reduced-motion users see static text.
  • ScrollTrigger is cleaned up in SPA routes.
  • GSAP初始化前文本处于隐藏状态,随后通过
    autoAlpha: 1
    变为可见。
  • 屏幕阅读器通过
    aria-label
    获取原始完整文本。
  • 文字间的空格得以保留。
  • 启用减少动效的用户会看到静态文本。
  • SPA路由中已清理ScrollTrigger。