本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ページ遷移

ページ遷移で幕が降り、中央にロゴが描かれてから幕が上がって次のページになる

デモ

操作:クリック。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

リンクが押されたら、画面の上からローズの幕を降ろして今のページを覆い、幕の中央でロゴの字の輪郭を左から線で描いていきます。描けた字から順に塗りに変わり、ロゴが出そろうと幕が上がって、下から次のページが少し持ち上がりながら現れます。次のページは幕が降りているあいだに読み込むので、ロゴを描く時間がそのまま読み込みの待ち時間になります。色面で拭うだけの遷移と違い、ページが切り替わるたびにブランドのロゴを見せられます。

幕は 0.65 秒の power3.inOut で降り、覆われていく今のページは 48px 下へずれます。輪郭は降り始めて 0.5 秒後から 1 字 0.5 秒で描き、字ごとに 0.028 秒ずつずらして左から描き進めます。塗りは描き始めて 0.42 秒後から 0.3 秒で入れ、同時に輪郭の線を消します。線を残したままだと、字が線の太さの半分だけ太って見えます。幕は、読み込みとロゴの描き終わりの両方を待ってから 0.65 秒で上がります。

ロゴは、字ごとの path で書いた SVG を幕の中央に置きます。各 path に pathLength を 1 と付けておくと、字の長さに関係なく、破線のずれを 1 から 0 へ動かすだけで同じ時間で描けます。穴のある字(A や ® など)は外側と内側の輪郭を 1 本の path にまとめ、evenodd で塗ると穴が埋まりません。1 本に輪郭が複数あるときは、それぞれの輪郭の頭から同時に描き始まり、短い輪郭ほど先に描き終わります。

SVG の text に破線をかけて字を描こうとすると、Chromium では字の大きさによって破線の長さの測り方が変わり、字ごとの描き始めと描き終わりがそろいません。文字のままではなく、アウトライン化した path のロゴを使ってください。線の太さを保つ vector-effect も、pathLength と合わずに描く前の点が残るので使わず、狭い画面では線を太くします。幕の色は、墨色のロゴと 4:1 近いコントラストがとれる、落ち着いたローズにしています。ブランドの色に替えるときは、ロゴの色とのコントラストを確かめてください。

つまみは、幕の色、幕が降りる・上がる時間、字ごとのずらし方と塗りに変わる時間、次のページが持ち上がる量です。差し替えたあとは、題名と URL を書き換え、読み上げとキーボードの位置を新しいページの見出しへ移します。動きを減らす設定では、同じ流れのまま、ほぼ一瞬で切り替えます。ファッションや香り、レストランのように、ロゴを印象づけたいブランドのサイトに向いています。

コード

HTML
<!-- 遷移で差し替えるのは data-page の中だけ。次のページも同じ形で書く -->
<div class="page" data-page>
  <header class="site-header">
    <a class="site-header__logo" href="/">Hi! I'm HIGASHIYUKI®</a>
    <nav aria-label="メイン">
      <ul class="site-header__links">
        <li><a href="/shop/">ショップ</a></li>
        <li><a href="/story/">ブランドについて</a></li>
        <li><a href="/journal/">読みもの</a></li>
        <li><a href="/contact/">お問い合わせ</a></li>
      </ul>
    </nav>
  </header>

  <main class="hero">
    <p class="hero__eyebrow">Home fragrance</p>
    <h1 class="hero__title">静かな部屋に、<br>木の香りを。</h1>
    <p class="hero__lead">ここに説明文が入ります。</p>
    <a class="button" href="/shop/">香りを選ぶ</a>
  </main>
</div>

<!-- 幕。body の末尾に 1 つだけ置く。ロゴは、自分のロゴの SVG の path に差し替える(1 字 1 本。穴のある字は輪郭をまとめて 1 本に) -->
<div class="curtain" aria-hidden="true">
  <svg class="curtain__logo" viewBox="0 0 778 51">
    <path pathLength="1" d="M2.4 4.5l-0.1 42.4l10.4 0l0.2-17.7l16 0l0.2 0.2l0.1 17.5l10.2 0.1l0-42.6l-10 0l-0.3 0.2l0 15.5l-0.2 0.4l-16 0l-0.3-16.1z"/>
    <path pathLength="1" d="M59.2 2.1l-2.5 0.9l-1.5 1.4l-0.9 2l0 2.2l0.9 2l1.7 1.6l2 0.7l2 0l2-0.7l1.8-1.6l0.8-2l0-2.2l-0.8-2l-1.5-1.4l-1.5-0.7zM55.2 17l-0.3 0.1l0.1 29.8l9.7 0.1l0.2-0.4l0-29.5z"/>
    <path pathLength="1" d="M81.6 4.5l-0.1 28.1l10.1 0.2l0.2-28.2l-0.2-0.2zM86.6 36.1l-2.2 0.4l-2 1.4l-1.4 2.2l-0.2 2.3l1 2.7l1.6 1.6l2 0.8l2.5 0l2-0.8l1.7-1.6l0.9-2l0-2.5l-0.8-2l-1.3-1.3l-1.5-0.8z"/>
    <path pathLength="1" d="M131.2 4.5l-0.1 42.4l10.3 0.1l0.2-42.4l-0.2-0.2z"/>
    <path pathLength="1" d="M158.2 4.6l0 16l7.8 0.2l0.1-16.2l-0.1-0.2z"/>
    <path pathLength="1" d="M198.8 16.3l-2.5 0.8l-1.7 1l-2.5 2.7l-0.3-3.8l-9.6 0.1l0.1 29.9l10-0.1l0-16l0.2-2.3l0.5-1.5l1.4-1.7l1.4-0.8l1.3-0.2l1.2 0.1l1.5 0.9l0.9 1.2l0.5 1.5l0.1 18.8l10 0l0-16l0.2-2.3l0.6-1.5l1.1-1.5l1.1-0.8l1.8-0.4l1.5 0.2l1.2 0.7l0.8 1.1l0.6 1.5l0.2 3l0 16l10 0l-0.2-20.8l-1.1-4l-2-3l-1.5-1.3l-2.8-1.3l-2-0.3l-3.2 0l-2.3 0.6l-1.7 0.8l-2 1.5l-1.8 1.9l-2.2-2.8l-2.5-1.4l-2.5-0.6z"/>
    <path pathLength="1" d="M267.7 4.5l-0.1 42.4l10.4 0l0.2-17.7l16 0l0.3 0.2l0 17.5l10.2 0.1l0-42.6l-10 0l-0.2 0.2l0 15.5l-0.3 0.4l-16 0l-0.3-16.1z"/>
    <path pathLength="1" d="M320.7 4.5l0 42.4l10.3 0.1l0.1-42.4l-0.1-0.2z"/>
    <path pathLength="1" d="M365.5 3.3l-5 1l-5.2 2.2l-3.8 2.9l-1.8 2l-2.1 3l-1.7 4.2l-0.7 3.5l-0.3 3.3l0.5 5.2l1.3 4.3l2.2 4l4.1 4.3l4 2.4l4.3 1.6l4.5 0.8l6 0l6.2-1.1l5.3-1.7l1.5-0.8l0-0.3l0-22.3l-17.6 0.1l0.1 8.2l8 0.3l0 7.2l-1 0.6l-2 0.5l-3.5 0.3l-2.8-0.3l-2.5-0.7l-2-1.1l-2.2-1.8l-2.1-3.2l-1-3.8l0-5l0.7-2.7l1-2l2.4-2.8l1.2-1.1l3-1.5l2.3-0.5l3 0l4.2 1.1l1.8 0.9l2.2 1.9l6.4-7.3l0.1-0.2l-1.2-1.1l-1.8-1.2l-5.5-2.3l-5.5-1z"/>
    <path pathLength="1" d="M412.8 4.5l-17.7 42.4l11.2 0.1l3-7.9l0.5-0.4l15.8 0l3.5 8.3l11.5-0.1l-17.5-42.3l-0.3-0.2zM417.8 16.3l-0.3 0.3l-4.9 13.6l10.3-0.1z"/>
    <path pathLength="1" d="M465.3 3.4l-2.3 0.1l-4.2 1l-4.3 2.3l-2.1 2.1l-1.2 1.7l-0.7 1.8l-0.6 2.5l0 3.5l0.5 2.5l1.4 2.5l1.5 1.7l3.5 2.3l10.2 3.9l1.9 1.6l0.5 1l0 2.2l-0.8 1.5l-1.8 1.4l-2 0.4l-2.3 0l-2.7-0.7l-2.8-1.5l-2.5-2.2l-6.8 6.9l2.3 2l3 1.8l4 1.5l4.3 0.8l4.5 0l2.2-0.3l3.3-0.9l3-1.6l1.9-1.6l1.7-2l1.5-3l0.5-2.5l0.1-2.2l-0.3-3.3l-0.8-2l-1.6-2.1l-1.8-1.7l-2-1.2l-11.2-4.4l-1.5-1.4l-0.5-1.7l0.2-1.2l1-1.6l1.8-1l2.7-0.4l1.8 0.2l2.2 0.7l1.8 1l2 1.7l6.5-6.9l-3-2.4l-3.8-1.7l-4-1z"/>
    <path pathLength="1" d="M494 4.5l-0.1 42.4l10.4 0l0.2-17.7l16 0l0.2 0.2l0.1 17.5l10.2 0.1l0-42.6l-10 0l-0.3 0.2l0 15.5l-0.2 0.4l-16 0l-0.3-16.1z"/>
    <path pathLength="1" d="M547 4.5l-0.1 42.4l10.3 0.1l0.2-42.4l-0.2-0.2z"/>
    <path pathLength="1" d="M567.8 4.5l15.8 24.6l0 17.8l10.2 0.1l0.2-17.9l16.2-24.5l-0.2-0.2l-11.7 0l-9 15.4l-9-15.3z"/>
    <path pathLength="1" d="M620.4 4.4l-0.3 0.2l0.1 28.5l1 4.5l2.1 3.8l2.3 2.5l3 2.1l3.8 1.4l3 0.6l5 0l3.7-0.5l3.5-1.3l2.8-1.7l2.3-2.1l1.8-2.5l0.9-2l1-3.5l0.3-3.5l-0.1-26.5l-10.2 0l-0.1 25.5l-0.4 3.2l-1 2.3l-1.5 1.8l-2.5 1.2l-2.5 0.3l-2.5-0.3l-2.5-1.2l-1.9-2.3l-0.7-2l-0.3-3l-0.1-25.5z"/>
    <path pathLength="1" d="M672.1 4.5l-0.1 42.4l10.2 0l0.1-19.3l15.8 19.4l13.4-0.1l-19.2-22.8l18-19.5l-0.2-0.2l-12.8 0.1l-15 17l-0.2-17.1z"/>
    <path pathLength="1" d="M722 4.5l-0.1 42.4l10.3 0.1l0.1-42.4l-0.1-0.2z"/>
    <path pathLength="1" d="M760 3.8l-3.5 1l-2.3 1.2l-2 1.6l-1.7 2.1l-1.4 2.4l-0.7 2.3l-0.3 2.7l0.3 3l0.7 2.5l1.3 2.3l1.8 2.2l2 1.6l2.3 1.2l2.5 0.8l2.7 0.3l3-0.3l2.5-0.7l2.3-1.2l2-1.7l1.7-2l1.4-2.5l0.7-2.2l0.3-3l-0.2-2.8l-0.8-2.5l-1.2-2.2l-1.9-2.3l-1.8-1.5l-2.5-1.3l-2.2-0.8l-2-0.2zM760.2 6.1l-2 0.5l-2.5 1.2l-1.9 1.6l-1.3 1.5l-1.8 4l-0.3 2.2l0.2 2.5l0.5 1.8l1.4 2.5l1.2 1.5l1.8 1.5l2 1.1l2.2 0.7l4.3 0l2.2-0.7l2-1.1l1.8-1.5l1.2-1.5l1.8-4l0.3-2.5l-0.2-2.3l-1.5-3.7l-1.6-2l-1.5-1.3l-3.8-1.8l-1.7-0.3zM755.5 9l-0.2 0.1l0 15.8l5.2 0.2l0.1-5.5l0.4-0.1l0.6 0.1l2.6 5.5l6-0.2l-3.4-6l1.7-1.5l0.9-1.8l0-2.7l-1.2-1.9l-2.2-1.5l-2.3-0.4zM760.7 13.3l0 2.2l2 0l0.9-0.6l0.1-0.8l-0.5-0.5z"/>
  </svg>
</div>

CSS
:root {
  --page-bg: #f2f1ed;
  --page-ink: #262626;
  --page-mute: #6b6b6b;
  --curtain: #cc6257; /* 幕の色。墨色のロゴとのコントラストは 3.9:1 */
  --logo-width: min(78vw, 790px); /* 幕の中央のロゴの幅 */
  --gutter: clamp(24px, 7vw, 96px);
}

body { margin: 0; background: var(--page-bg); }

.page {
  min-height: 100vh;
  background: var(--page-bg);
  color: var(--page-ink);
}

/* 幕。画面に固定し、降ろすときだけ見せる(見せる・隠すと位置は JS が動かす) */
.curtain {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  background: var(--curtain);
  box-shadow: 0 24px 64px rgb(69 27 23 / 0.22);
  visibility: hidden;
}

.curtain__logo { width: var(--logo-width); height: auto; overflow: visible; color: var(--page-ink); }

/* ロゴの字。輪郭の線を描いてから塗る。pathLength="1" なので、破線のずれを 1 → 0 と動かすと描ける(ずれと濃さは JS が style で書く) */
.curtain__logo path {
  fill: currentColor;
  fill-opacity: 0;
  fill-rule: evenodd; /* 1 本にまとめた内側の輪郭(A や ® の穴)を塗らない */
  stroke: currentColor;
  stroke-width: 1.4; /* viewBox の単位。ロゴと一緒に拡大・縮小する */
  stroke-linejoin: round;
  stroke-dasharray: 1 1;
  stroke-dashoffset: 1;
}

/* 狭い画面ではロゴが小さくなるので、線を太くしておく(vector-effect: non-scaling-stroke は pathLength と合わず、描く前の点が残る) */
@media (max-width: 600px) {
  .curtain__logo path { stroke-width: 2.8; }
}

.site-header {
  display: flex;
  flex-wrap: wrap; /* 狭い画面では、和文のナビをロゴの下の段へ送る */
  align-items: center;
  gap: 16px 32px;
  padding: 32px var(--gutter);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.site-header__logo { margin-right: auto; font-size: 18px; font-weight: 700; letter-spacing: 0.12em; white-space: nowrap; }
.site-header__links { display: flex; flex-wrap: wrap; gap: 8px 32px; margin: 0; padding: 0; list-style: none; }
.site-header a { color: var(--page-mute); text-decoration: none; white-space: nowrap; }
.site-header .site-header__logo,
.site-header a[aria-current="page"] { color: var(--page-ink); }
.site-header a[aria-current="page"] { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px; }

.hero { padding: 96px var(--gutter); }

.hero__eyebrow {
  margin: 0 0 24px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--page-mute);
}

.hero__title { margin: 0; font-size: clamp(44px, 7vw, 72px); font-weight: 800; line-height: 1.05; letter-spacing: -0.02em; }
.hero__lead { margin: 24px 0 40px; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }

.button {
  display: inline-flex;
  align-items: center;
  height: 48px;
  padding: 0 32px;
  border-radius: 999px;
  background: var(--page-ink);
  color: #ffffff;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-decoration: none;
}

.page a:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }

/* 遷移のあとに見出しへ移したフォーカスは、枠を出さない(読み上げの位置を移すためだけのもの) */
.page h1[tabindex="-1"]:focus { outline: none; }

JavaScript
import { gsap } from "gsap";

const EACH = 0.028; // 字ごとに描き始めをずらす時間(秒)。1 字は 0.5 秒で描く
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const curtain = document.querySelector(".curtain");
const letters = curtain.querySelectorAll(".curtain__logo path"); // ロゴの字(左から順に並べておく)
let busy = false;
let shownPath = location.pathname; // いま見せているページ

history.scrollRestoration = "manual"; // スクロール位置は、差し替えたあとに自分で戻す

// 次のページの HTML を読み、差し替える部分(data-page)とタイトルを取り出す
async function load(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(response.statusText);
  const doc = new DOMParser().parseFromString(await response.text(), "text/html");
  const page = doc.querySelector("[data-page]");
  if (!page) throw new Error("data-page が無い");
  return { page, title: doc.title };
}

async function swap(url, push) {
  const current = document.querySelector("[data-page]");
  // 幕を降ろしながら、ロゴの輪郭を左の字から線で描き、描けた字から塗りに変える。そのあいだに次のページを読む
  const cover = gsap
    .timeline()
    .timeScale(reduceMotion ? 20 : 1) // 動きを減らす設定では、ほぼ一瞬で切り替える
    .fromTo(curtain, { yPercent: -100, autoAlpha: 1 }, { yPercent: 0, duration: 0.65, ease: "power3.inOut" })
    .to(current, { y: 48, duration: 0.65, ease: "power3.inOut" }, 0) // 覆われていくページは少し下へ
    .fromTo(letters, { strokeDashoffset: 1, strokeOpacity: 1, fillOpacity: 0 }, { strokeDashoffset: 0, duration: 0.5, ease: "power1.inOut", stagger: EACH }, 0.5)
    // 塗りに変わるほど線を消す(線を残すと、字が線の太さの半分だけ太って見える)
    .to(letters, { fillOpacity: 1, strokeOpacity: 0, duration: 0.3, ease: "power1.inOut", stagger: EACH }, 0.92);
  const [{ page, title }] = await Promise.all([load(url), cover]);
  if (push) history.pushState({}, "", url);
  shownPath = new URL(url).pathname;
  current.replaceWith(page);
  document.title = title;
  scrollTo(0, 0);
  // 読み上げとキーボードの位置を、新しいページの見出しへ移す
  const heading = page.querySelector("h1");
  heading?.setAttribute("tabindex", "-1");
  heading?.focus({ preventScroll: true });
  // 幕を上げ、次のページを下から少し持ち上げる
  await gsap
    .timeline()
    .timeScale(reduceMotion ? 20 : 1)
    .to(curtain, { yPercent: -100, duration: 0.65, ease: "power3.inOut" }, 0.18)
    .fromTo(page, { y: 64 }, { y: 0, duration: 0.7, ease: "power3.out", clearProps: "transform" }, 0.23)
    .set(curtain, { autoAlpha: 0 });
}

async function go(url, push) {
  if (busy) return;
  busy = true;
  await swap(url, push).catch(() => location.assign(url)); // 読み込めなかったら、ふつうに移る
  busy = false;
}

// 同じサイトのほかのページへのリンクだけ横取りする(別タブ・修飾キー付き・同じページ内のリンクは邪魔しない)
document.addEventListener("click", (event) => {
  const link = event.target.closest("a[href]");
  if (!link || event.defaultPrevented || event.button !== 0) return;
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || link.target || link.hasAttribute("download")) return;
  const url = new URL(link.href);
  if (url.origin !== location.origin || url.pathname === location.pathname) return;
  event.preventDefault();
  go(url.href, true);
});

// ブラウザの戻る・進むでも、同じ幕で移る(同じページの中の移動は除く)
addEventListener("popstate", () => {
  if (location.pathname !== shownPath) go(location.href, false);
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。