ドロワー
ボタンで右側からパネルがスライドイン。背景クリックか×で閉じる。モバイルではボトムシートになる想定のスタイル。
プレビュー
操作クリック/タップで動きます。
プレビューは枠内に収めるため、オーバーレイを position:absolute(ステージ基準)で実装しています。実際のページで画面全体に重ねるときは position:fixed に変え、body 直下など高い階層へ置いてください。
使いどころ
グローバルナビ・フィルタ設定・カート内容の確認など「一覧やメニューの延長」を画面外から呼び出す用途に向く。モーダルと違って判断を強制する場面ではなく、開いたまま元の画面を横目にできる軽さが持ち味なので、Yes/Noのような決定を求める内容には向かない(それはモーダルの役目)。ハンバーガーメニューの受け皿として使うのが典型で、項目数が多いナビをヘッダーに並べきれないときに有効。逆に、項目が2〜3個しかないメニューにはポップオーバーの方が軽くて動線も短い。また、デスクトップの広い画面で常時表示できるサイドバーがあるなら、わざわざオーバーレイのドロワーにする必要はない。画面幅に応じてサイドバー⇄ドロワーを切り替える設計が定番になる。
仕組み
モーダルと同じ backdrop+パネルの重ね方だが、パネルは transform: translateX(100%) で右側の画面外に置き、開くと translateX(0) へ動かすスライド方式。opacity は使わずtransformだけで隠しているため、閉じている間も要素はレイアウト上に存在し続ける。box-shadow は閉じているときだけ none にしている点が特徴的で、これは影のぼかしが overflow:hidden の右端からはみ出し、パネル本体が完全に隠れているのに縁だけ「暗い帯」として見えてしまう現象を避けるための処理。開くときのフォーカス移動は items[0].focus({ preventScroll: true }) としており、スライドイン中にフォーカスの副作用でステージ自体が横スクロールしてボタン位置がずれることを防いでいる。閉じるときはモーダルと同じく trigger 要素へフォーカスを戻す。
実装の注意
opacity ではなく transform だけで隠しているため、ドロワーが閉じている状態でも中のリンクはキーボードのTabで到達できてしまう。画面には見えないのに、Tabを連打すると突然「ホーム」「プロフィール」などのリンクにフォーカスが当たり、しかも画面外にあるぶんブラウザが要素を可視域に入れようとしてステージが意図せず横スクロールすることがある。対策はモーダルと同じで、閉じたときに inert(非対応環境では tabindex="-1" の一括付与)を当てること。もう一つは box-shadow の付け外し忘れで、これを両方の状態に常時適用すると、閉じている間もパネルの縁の影だけがうっすら見え続けるバグになる。影は is-open のときだけ有効化する設計を崩さないこと。
アクセシビリティ
role="dialog" と aria-modal="true" を付け、タイトル文言がテキストノードで aria-labelledby に紐づけにくいぶん aria-label="サイドメニュー" で直接ラベルしている。フォーカストラップ・Escapeでの終了・閉じた後のフォーカス復帰はモーダルと同じ実装で、ナビゲーション用途なので中身は nav 要素でマークアップし、リンクの並び順がそのままTab順になるようにしてある。前述の「非表示でもフォーカス可能」な状態は aria-hidden="true" を付与した要素に focusable な子要素が残る、という ARIA仕様違反でもあるため、閉じた瞬間に inert 化することが見た目だけでなく支援技術対応としても必須になる。prefers-reduced-motion ではスライドのトランジションを止めて開閉が瞬時に切り替わるようにしている。
調整して使う
主に触るのは3つ。開く方向(右からが定番だが、左メニューのアプリでは左から、RTL言語では逆にする)、幅(デモは68%・最大160pxだが、実運用では280〜320px程度かビューポート幅の75〜85%が読みやすい)、そして背景の扱い。今回はオーバーレイ方式(本体の上に重ねる)だが、メインコンテンツを横に押し出す「プッシュ方式」に変えることもできる。ただしプッシュ方式は本体側に margin や transform を掛けてレイアウトのリフローを伴うぶん実装コストが増えるため、パフォーマンスを優先するならこのままのオーバーレイ方式を維持し、開閉の速さで軽さを出す方が扱いやすい。
コード(コピーして使えます)
<div class="stage" id="stage">
<button class="open-btn" id="openDrawer">メニューを開く</button>
<div class="backdrop" id="backdrop" aria-hidden="true"></div>
<div class="drawer" id="drawer" role="dialog" aria-modal="true" aria-label="サイドメニュー" aria-hidden="true">
<div class="drawer-header">
<p class="drawer-title">メニュー</p>
<button class="close-btn" id="closeDrawer" aria-label="閉じる">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<line x1="3" y1="3" x2="13" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="13" y1="3" x2="3" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<nav class="drawer-nav">
<a class="drawer-item" href="#">ホーム</a>
<a class="drawer-item" href="#">プロフィール</a>
<a class="drawer-item" href="#">設定</a>
<a class="drawer-item" href="#">ヘルプ</a>
</nav>
</div>
</div>