ナビゲーション
タブ切替コンテンツスイッチャー
複数視点の情報をタブで切り替え、キー操作と初期表示の一貫性を確認できるデザインです。
動きを試す
プレビュー
必要なファイルを選ぶ
コード
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画面で切り替えて確認する
- 一覧・設定・履歴などの区画を見せる必要がある
- 表示文脈を切り替えた際のフォーカス維持を確認する
適さない場面
- 同時編集が必要な複雑なフォーム状態管理
- 大量のタブを同時表示しないといけないケース
