ナビゲーション

パンくず階層ナビゲーション

利用者の現在地と上位階層への導線を、可読性の高いパンくずとして切り替えて確認できるレシピです。

動きを試す

プレビュー

プレビュー設定

プレビューを準備しています。

必要なファイルを選ぶ

コード

index.html

利用前に変更する箇所

  • 項目文言と各画面階層のラベルを実装環境向けに置き換える

<!doctype html>
<html lang="ja" data-theme="light">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>パンくず階層ナビゲーション</title>
    <style>
      :root {
        --bg: #f7f8fa;
        --surface: #ffffff;
        --surface-muted: #eff2f6;
        --text: #18212f;
        --muted: #5d6b7a;
        --border: #d7dee8;
        --accent: #245fd6;
        --focus: #0077cc;
      }

      :root[data-theme="dark"] {
        --bg: #0f141a;
        --surface: #171d26;
        --surface-muted: #202834;
        --text: #f2f5f8;
        --muted: #aab4c0;
        --border: #354152;
        --accent: #7ea7ff;
        --focus: #65c7ff;
      }

      * {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        padding: 18px;
        background: var(--bg);
        color: var(--text);
        font-family: Arial, sans-serif;
      }

      .panel {
        max-width: 720px;
        margin: 0 auto;
        padding: 18px;
        border: 1px solid var(--border);
        background: var(--surface);
        border-radius: 12px;
      }

      .eyebrow {
        margin: 0 0 8px;
        color: var(--muted);
        font-size: 0.9rem;
      }

      .breadcrumb {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        align-items: center;
        list-style: none;
        margin: 10px 0;
        padding: 0;
      }

      .breadcrumb li {
        display: flex;
        align-items: center;
        gap: 6px;
      }

      .breadcrumb a {
        color: var(--accent);
        text-decoration: none;
      }

      .breadcrumb [aria-current="page"] {
        color: var(--text);
        font-weight: 700;
      }

      [data-state-panel][hidden] {
        display: none;
      }

      .state-panel {
        border: 1px solid var(--border);
        border-radius: 10px;
        padding: 10px 12px;
        background: var(--surface-muted);
      }

      .toolbar {
        margin-top: 14px;
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
      }

      button {
        min-height: 44px;
        padding: 8px 14px;
        border-radius: 8px;
        border: 1px solid var(--border);
        background: var(--surface);
        color: var(--text);
        cursor: pointer;
      }

      button:focus-visible {
        outline: 3px solid var(--focus);
      }

      .status {
        margin-top: 10px;
        min-height: 1.4em;
      }

      @media (max-width: 360px) {
        body {
          padding: 10px;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        *,
        *::before,
        *::after {
          animation: none !important;
          transition-duration: 0.01ms !important;
        }
      }
    </style>
  </head>
  <body>
    <main class="panel" id="breadcrumb-root">
      <p class="eyebrow" id="breadcrumb-section">パンくず導線デモ</p>

      <section data-state-panel="default" aria-labelledby="default-state">
        <h1 id="default-state">通常状態</h1>
        <p>現在地を3階層で示します。</p>
        <ol class="breadcrumb">
          <li><a href="#breadcrumb-root">導線の一覧</a><span>›</span></li>
          <li><a href="#breadcrumb-section">資料領域</a><span>›</span></li>
          <li><span data-current-page>現在のページ</span></li>
        </ol>
      </section>

      <section data-state-panel="compact" aria-labelledby="compact-state" hidden>
        <h2 id="compact-state">折りたたみ状態</h2>
        <p>中間階層を省略して主要導線を残します。</p>
        <ol class="breadcrumb">
          <li><a href="#breadcrumb-root">導線</a><span>›</span></li>
          <li><span>…</span><span>›</span></li>
          <li><span data-current-page>現在のページ</span></li>
        </ol>
      </section>

      <section data-state-panel="active" aria-labelledby="active-state" hidden>
        <h2 id="active-state">現在地強調</h2>
        <p>最終項目をより目立つ表現にしています。</p>
        <ol class="breadcrumb">
          <li><a href="#breadcrumb-root">導線の一覧</a><span>›</span></li>
          <li><a href="#breadcrumb-section">資料領域</a><span>›</span></li>
          <li><strong data-current-page>現在のページ</strong></li>
        </ol>
      </section>

      <div class="toolbar">
        <button type="button" data-set-state="default">default</button>
        <button type="button" data-set-state="compact">compact</button>
        <button type="button" data-set-state="active">active</button>
      </div>
      <p role="status" class="status" data-status></p>
    </main>

    <script>
      (() => {
        const params = new URLSearchParams(window.location.search);
        const instance = params.get("instance") ?? "missing-instance";
        const panels = Array.from(document.querySelectorAll("[data-state-panel]"));
        const status = document.querySelector("[data-status]");
        const buttons = document.querySelectorAll("[data-set-state]");
        const currentPages = document.querySelectorAll("[data-current-page]");
        const validStates = new Set(["default", "compact", "active"]);

        const setState = (nextState) => {
          if (!validStates.has(nextState)) return;
          document.documentElement.dataset.state = nextState;
          for (const panel of panels) {
            panel.hidden = panel.dataset.statePanel !== nextState;
          }
          for (const currentPage of currentPages) {
            const panel = currentPage.closest("[data-state-panel]");
            if (panel?.dataset.statePanel === nextState) {
              currentPage.setAttribute("aria-current", "page");
            } else {
              currentPage.removeAttribute("aria-current");
            }
          }
          if (status) {
            status.textContent = `状態: ${nextState}`;
          }
        };

        const setTheme = (theme) => {
          if (theme === "dark" || theme === "light") {
            document.documentElement.dataset.theme = theme;
          }
        };

        for (const button of buttons) {
          button.addEventListener("click", () => setState(button.dataset.setState ?? "default"));
        }

        window.addEventListener("message", (event) => {
          if (
            event.source !== window.parent ||
            typeof event.data !== "object" ||
            event.data === null
          ) {
            return;
          }
          if (event.data.source !== "wpl-host" || event.data.instance !== instance) {
            return;
          }
          if (event.data.type === "wpl-preview-state") {
            setState(event.data.value);
          }
          if (event.data.type === "wpl-preview-theme") {
            setTheme(event.data.value);
          }
        });

        setTheme(params.get("theme") === "dark" ? "dark" : "light");
        setState(validStates.has(params.get("state")) ? (params.get("state") ?? "default") : "default");

        window.parent.postMessage(
          { source: "wpl-preview", type: "wpl-preview-ready", instance },
          "*",
        );
      })();
    </script>
  </body>
</html>
プレビューで試せる動き
  • 通常表示: 3階層以上を1行で示します。
  • 折りたたみ: スマートフォン向けに中間階層を省略します。
  • 最終項目強調: 現在地を太字とアイコンで示します。

使い分けを確認する

このレシピが合う場面

適する場面

  • 情報階層が深い画面で現在地の文脈を示す
  • 一覧→詳細へ移動する画面で戻る導線を明確化する
  • 編集画面やステータス更新画面で導線を確認する

適さない場面

  • 階層が存在しない単一画面のみの導線
  • 複数階層でなくタグ一覧のみを扱う構造