ナビゲーション

タブ切替コンテンツスイッチャー

複数視点の情報をタブで切り替え、キー操作と初期表示の一貫性を確認できるデザインです。

動きを試す

プレビュー

プレビュー設定

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

必要なファイルを選ぶ

コード

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;
        --text: #18212f;
        --muted: #5d6b7a;
        --border: #d7dee8;
        --accent: #245fd6;
        --focus: #0077cc;
      }
      :root[data-theme="dark"] {
        --bg: #0f141a;
        --surface: #171d26;
        --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;
      }
      .wrap {
        max-width: 760px;
        margin: 0 auto;
        padding: 14px;
        border: 1px solid var(--border);
        border-radius: 12px;
        background: var(--surface);
      }
      .tabs {
        display: flex;
        gap: 6px;
        flex-wrap: wrap;
      }
      button {
        border: 1px solid var(--border);
        border-radius: 8px;
        padding: 8px 14px;
        background: var(--surface);
        color: var(--text);
        min-height: 44px;
      }
      button[aria-selected="true"] {
        border-color: var(--accent);
        font-weight: 700;
      }
      button:focus-visible {
        outline: 3px solid var(--focus);
      }
      [data-state-panel][hidden] {
        display: none;
      }
      .panel {
        margin-top: 12px;
        border: 1px solid var(--border);
        border-radius: 10px;
        padding: 12px;
      }
      .muted {
        color: var(--muted);
      }
      @media (max-width: 360px) {
        body {
          padding: 10px;
        }
      }
      @media (prefers-reduced-motion: reduce) {
        * {
          transition: none !important;
        }
      }
    </style>
  </head>
  <body>
    <main class="wrap">
      <h1>コンテンツ切替タブ</h1>
      <div class="tabs" role="tablist" aria-label="表示内容">
        <button id="tab-overview" type="button" role="tab" aria-controls="panel-overview" aria-selected="true" tabindex="0" data-set-state="overview">概要</button>
        <button id="tab-details" type="button" role="tab" aria-controls="panel-details" aria-selected="false" tabindex="-1" data-set-state="details">詳細</button>
        <button id="tab-history" type="button" role="tab" aria-controls="panel-history" aria-selected="false" tabindex="-1" data-set-state="history">変更内容</button>
      </div>
      <section id="panel-overview" class="panel" role="tabpanel" aria-labelledby="tab-overview" data-state-panel="overview">
        <h2>概要</h2>
        <p>この状態では要約情報を先頭に配置します。</p>
      </section>
      <section id="panel-details" class="panel" role="tabpanel" aria-labelledby="tab-details" data-state-panel="details" hidden>
        <h2>詳細</h2>
        <p>補足条件・更新日時などの内容を表示します。</p>
      </section>
      <section id="panel-history" class="panel" role="tabpanel" aria-labelledby="tab-history" data-state-panel="history" hidden>
        <h2>変更内容</h2>
        <p class="muted">操作履歴ではなく利用者向けの変更内容を表示します。</p>
      </section>
      <p role="status" data-status></p>
    </main>
    <script>
      (() => {
        const params = new URLSearchParams(window.location.search);
        const instance = params.get("instance") ?? "missing-instance";
        const statePanels = Array.from(document.querySelectorAll("[data-state-panel]"));
        const status = document.querySelector("[data-status]");
        const buttons = Array.from(document.querySelectorAll("[data-set-state]"));
        const validStates = new Set(["overview", "details", "history"]);
        const setState = (nextState) => {
          if (!validStates.has(nextState)) return;
          document.documentElement.dataset.state = nextState;
          for (const panel of statePanels) {
            panel.hidden = panel.dataset.statePanel !== nextState;
          }
          for (const button of buttons) {
            const selected = button.dataset.setState === nextState;
            button.setAttribute("aria-selected", String(selected));
            button.tabIndex = selected ? 0 : -1;
          }
          const selectedTab = buttons.find((button) => button.dataset.setState === nextState);
          if (status && selectedTab) status.textContent = `${selectedTab.textContent?.trim()}を表示しています。`;
        };
        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 ?? "overview"));
          button.addEventListener("keydown", (event) => {
            const currentIndex = buttons.indexOf(button);
            let nextIndex = currentIndex;
            if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % buttons.length;
            else if (event.key === "ArrowLeft") nextIndex = (currentIndex - 1 + buttons.length) % buttons.length;
            else if (event.key === "Home") nextIndex = 0;
            else if (event.key === "End") nextIndex = buttons.length - 1;
            else return;
            event.preventDefault();
            const nextTab = buttons[nextIndex];
            setState(nextTab.dataset.setState ?? "overview");
            nextTab.focus();
          });
        }
        window.addEventListener("message", (event) => {
          if (
            event.source !== window.parent ||
            typeof event.data !== "object" ||
            event.data === null ||
            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") ?? "overview") : "overview");
        window.parent.postMessage({ source: "wpl-preview", type: "wpl-preview-ready", instance }, "*");
      })();
    </script>
  </body>
</html>
プレビューで試せる動き
  • 概要: 概要情報を最優先して表示します。
  • 詳細: 詳細情報と補足文言を表示します。
  • 変更内容: 利用者向けの最近の変更内容を確認できます。

使い分けを確認する

このレシピが合う場面

適する場面

  • 複数の情報ビューを1画面で切り替えて確認する
  • 一覧・設定・履歴などの区画を見せる必要がある
  • 表示文脈を切り替えた際のフォーカス維持を確認する

適さない場面

  • 同時編集が必要な複雑なフォーム状態管理
  • 大量のタブを同時表示しないといけないケース