mcpField
M
masked-reveal

masked-reveal

github.com/mengtoデザイン・UI7.1k1.7k 回インストール3 日前に更新
リポジトリ

masked-reveal について

GSAP と ScrollTrigger で、マスク越しに文字を順に表示するデザインスキルです。短い見出しやセクション導入向けに、初期化、アクセシビリティ、クリーンアップ、動きの既定値を扱います。

主な機能

  1. 01スクロール時にマスク越しで文字を表示
  2. 02GSAPによる単語ごとの段階表示
  3. 03空白とアクセシブルなテキストの保持
  4. 04動きを減らす設定とルートのクリーンアップ

活用シーン

  1. 01ランディングページの見出しをアニメーション化
  2. 02スクロールに合わせたセクション導入の表示

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mengto/skills --skill masked-reveal

インストール後、Claude Code で/masked-revealを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

demo/
index.html
PROMPT.md
source.json

詳細

マスク付きリビール

使用する場面

  • スクロール時に、見出しや短いテキストブロックを上質に表示したい場合。
  • 見えないマスクを通って、単語が順番にせり上がるようにしたい場合。
  • プロジェクトでGSAPをすでに使用している場合、またはScrollTriggerを使ったモーションが必要な場合。

モーションのデフォルト設定

  • トリガー:テキストの上端がビューポートの82%に達したときに開始します。
  • デュレーション:0.7s~0.9s。
  • スタッガー:単語ごとに0.025s~0.045s。
  • オフセット:yPercent: 110から0。
  • イージング:power3.outまたはexpo.out。
  • リプレイ:デフォルトでは1回だけ表示します。

HTML

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

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

このヘルパーを使うと、有料のSplitTextプラグインを避けつつ、スペースをそのまま維持できます。

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のクリーンアップパターン

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

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

スタイルのルール

  • 短い見出し、ラベル、セクションの導入文に使用し、長い段落は避けます。
  • 縦方向のオフセットはすっきりと保ちます。スタイル上明確に必要な場合を除き、ぼかしと組み合わせないでください。
  • 落ち着いたエディトリアルな雰囲気にするには、文字単位ではなく単語単位でスタッガーさせます。
  • 行の折り返しが重要な場合は、フォントの読み込み後に初期化します。
  • 画像の読み込みが遅れた場合やレイアウトが変化した場合は、ScrollTrigger.refresh()を使用します。
  • リンク、ボタン、または意味のあるインラインマークアップを含むテキストは分割しないでください。

簡易チェック

  • GSAPの初期化前はテキストが非表示になり、その後autoAlpha: 1で表示されます。
  • スクリーンリーダーには、aria-labelを通じて元の全文が伝わります。
  • 単語間のスペースが維持されます。
  • 視差効果を減らす設定のユーザーには、静的なテキストが表示されます。
  • SPAのルート切り替え時にScrollTriggerがクリーンアップされます。

コメント