ナビゲーション
モバイルドロワー
モバイル用に主要導線を抽出し、開閉・背景固定・戻る操作を確認できるドロワーメニューです。
動きを試す
プレビュー
必要なファイルを選ぶ
コード
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-subtle:#eff2f6; --text:#18212f; --muted:#5d6b7a; --border:#d7dee8; --accent:#245fd6; --focus:#0077cc; }
:root[data-theme="dark"] { --bg:#0f141a; --surface:#171d26; --surface-subtle:#202834; --text:#f2f5f8; --muted:#aab4c0; --border:#354152; --accent:#7ea7ff; --focus:#65c7ff; }
* { box-sizing: border-box; }
body { margin: 0; padding: 16px; font-family: Arial, sans-serif; background: var(--bg); color: var(--text); }
body[data-drawer-open="true"] { overflow: hidden; }
.layout { max-width: 700px; margin: 0 auto; }
.header { display: flex; justify-content: space-between; align-items: center; padding: 10px; border:1px solid var(--border); border-radius: 12px; background: var(--surface); }
.drawer { position: fixed; inset: 0; }
.drawer[hidden] { display: none; }
.backdrop {
position: absolute; inset: 0; background: rgba(15,20,26,.35); opacity: 0; transition: opacity .15s ease;
}
.panel {
position: absolute; top: 0; right: 0; width: min(280px, 80%); height: 100%; background: var(--surface); border-left: 1px solid var(--border); transform: translateX(100%); transition: transform .15s ease;
padding: 12px;
}
body[data-drawer-open="true"] .backdrop { opacity: 1; }
body[data-drawer-open="true"] .panel { transform: translateX(0); }
button { min-height: 44px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); padding: 8px 12px; }
.status { min-height: 1.4em; margin-top: 8px; }
.no-script-navigation { margin-top: 16px; padding: 12px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); }
button:focus-visible { outline: 3px solid var(--focus); }
[data-state-panel][hidden] { display: none; }
@media (max-width: 360px) { body { padding: 10px; } }
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}
</style>
</head>
<body>
<main class="layout">
<section class="header">
<h1>モバイルドロワー</h1>
<button type="button" id="toggle" aria-controls="mobileDrawer" aria-expanded="false">メニューを開く</button>
</section>
<div id="content-overview" data-state-panel="closed">
<p>閉じた状態。ページはそのまま使えます。</p>
</div>
<div id="content-usage" data-state-panel="open" hidden>
<p>開いた状態。移動候補を一覧表示します。</p>
</div>
<div id="content-settings" data-state-panel="selected" hidden>
<p>項目を選択しました。メニューを閉じて本文に復帰します。</p>
</div>
<div class="status" role="status" data-status></div>
</main>
<noscript>
<nav class="layout no-script-navigation" aria-label="JavaScriptなしのメニュー">
<p>JavaScriptを使わずに、各項目の概要を確認できます。</p>
<ul>
<li><a href="#fallback-content-overview">概要</a></li>
<li><a href="#fallback-content-usage">利用方法</a></li>
<li><a href="#fallback-content-settings">設定</a></li>
</ul>
<section id="fallback-content-overview">
<h2>概要</h2>
<p>ページ全体の案内を確認します。</p>
</section>
<section id="fallback-content-usage">
<h2>利用方法</h2>
<p>項目一覧から必要な案内へ移動します。</p>
</section>
<section id="fallback-content-settings">
<h2>設定</h2>
<p>設定に関する案内を確認します。</p>
</section>
</nav>
</noscript>
<aside class="drawer" id="mobileDrawer" data-drawer hidden>
<div class="backdrop" id="backdrop" data-backdrop></div>
<div class="panel">
<h2>メニュー</h2>
<ul style="padding-left: 18px">
<li><a href="#content-overview">概要</a></li>
<li><a href="#content-usage">利用方法</a></li>
<li><a href="#content-settings">設定</a></li>
</ul>
<button type="button" data-drawer-close>メニューを閉じる</button>
</div>
</aside>
<script>
(() => {
const params = new URLSearchParams(window.location.search);
const instance = params.get('instance') ?? 'missing-instance';
const states = new Set(['closed', 'open', 'selected']);
const status = document.querySelector('[data-status]');
const panels = Array.from(document.querySelectorAll('[data-state-panel]'));
const drawer = document.querySelector('[data-drawer]');
const toggle = document.getElementById('toggle');
const backdrop = document.querySelector('[data-backdrop]');
const closeButton = document.querySelector('[data-drawer-close]');
const links = Array.from(drawer.querySelectorAll('a'));
const updateStateDisplay = (nextState, message) => {
document.documentElement.dataset.state = nextState;
for (const panel of panels) panel.hidden = panel.dataset.statePanel !== nextState;
if (status) status.textContent = message;
};
const openDrawer = () => {
drawer.hidden = false;
document.body.dataset.drawerOpen = 'true';
toggle.setAttribute('aria-expanded', 'true');
updateStateDisplay('open', 'メニューを開きました。');
links[0]?.focus();
};
const closeDrawer = ({ selectedLabel = '', restoreFocus = true } = {}) => {
drawer.hidden = true;
delete document.body.dataset.drawerOpen;
toggle.setAttribute('aria-expanded', 'false');
const nextState = selectedLabel ? 'selected' : 'closed';
updateStateDisplay(nextState, selectedLabel ? `${selectedLabel}を選択しました。` : 'メニューを閉じました。');
if (restoreFocus) toggle.focus();
};
const setState = (nextState, { restoreFocus = false } = {}) => {
if (!states.has(nextState)) return;
if (nextState === 'open') openDrawer();
else if (nextState === 'selected') closeDrawer({ selectedLabel: '設定', restoreFocus });
else closeDrawer({ restoreFocus });
};
const setTheme = (theme) => {
if (theme === 'dark' || theme === 'light') document.documentElement.dataset.theme = theme;
};
toggle.addEventListener('click', () => {
if (drawer.hidden) openDrawer();
else closeDrawer();
});
closeButton.addEventListener('click', () => closeDrawer());
backdrop.addEventListener('click', () => closeDrawer());
for (const link of links) {
link.addEventListener('click', (event) => {
event.preventDefault();
closeDrawer({ selectedLabel: link.textContent?.trim() ?? '' });
});
}
drawer.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
closeDrawer();
return;
}
if (event.key !== 'Tab') return;
const controls = [...links, closeButton];
const first = controls[0];
const last = controls.at(-1);
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last?.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first?.focus();
}
});
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(states.has(params.get('state')) ? (params.get('state') ?? 'closed') : 'closed', { restoreFocus: false });
window.parent.postMessage({ source: 'wpl-preview', type: 'wpl-preview-ready', instance }, '*');
})();
</script>
</body>
</html>
プレビューで試せる動き
- 閉じた状態: メニュー非表示で本文のみ提示する
- 開いた状態: 本文を固定しメニュー項目を表示する
- 選択済み: 項目選択後にメニューが戻る状態
使い分けを確認する
このレシピが合う場面
適する場面
- 画面幅が狭い際に主要導線をまとめる
- モバイル時の背景スクロール抑止を確認する
- メニュー項目選択後の状態復帰を検証する
適さない場面
- 常に複数ペインが必要なデスクトップ主体画面
- ドラッグ式サイドメニューが必要な高度な操作
