画面端からスライドして出る panel コンポーネントです。
Source Code
<button type="button" data-ui-sheet-open="right-sheet">右からシートを開く</button>
<ui-sheet class="ui:sheet" id="right-sheet" aria-labelledby="right-sheet-title">
<header class="ui:sheet__header">
<h2 id="right-sheet-title">設定</h2>
<button type="button" data-ui-sheet-close aria-label="閉じる">×</button>
<div class="ui:sheet__body">
<footer class="ui:sheet__footer">
<button type="button" data-ui-sheet-close>キャンセル</button>
<button type="button">保存</button>
Source Code
<button type="button" data-ui-sheet-open="left-sheet">左からシートを開く</button>
<ui-sheet class="ui:sheet" id="left-sheet" data-side="left" aria-labelledby="left-sheet-title">
<header class="ui:sheet__header">
<h2 id="left-sheet-title">メニュー</h2>
<button type="button" data-ui-sheet-close aria-label="閉じる">×</button>
<div class="ui:sheet__body">
Source Code
<button type="button" data-ui-sheet-open="wide-sheet">広めのシートを開く</button>
<ui-sheet class="ui:sheet" id="wide-sheet" style="--ui-sheet-size: min(48rem, 100% - 2rem)" aria-labelledby="wide-sheet-title">
<header class="ui:sheet__header">
<h2 id="wide-sheet-title">ワイドシート</h2>
<button type="button" data-ui-sheet-close aria-label="閉じる">×</button>
<div class="ui:sheet__body">
<p>幅は CSS 変数 <code>--ui-sheet-size</code> で個別に上書き可能。</p>
| 属性 / CSS変数 | 要素 | 値 | 効果 |
|---|
data-side | root | "right" (既定) / "left" | スライドする方向 |
--ui-sheet-size | root | CSS の <length> | 幅。既定 min(28rem, 100% - 2rem) を上書き可能 |
| 属性 | 要素 | 値 | 効果 |
|---|
data-ui-sheet-open | 任意 | 対象 sheet の id | クリックで対応する <ui-sheet> を開く |
data-ui-sheet-close | 任意 | - | <ui-sheet> 内のクリックでそのシートを閉じる |
| 操作 | 動作 |
|---|
| Escape Esc Escape Esc | シートを閉じる |
| Tab Tab Tab Tab | シート内のフォーカス移動(外には出ない) |
| backdrop クリック | シートを閉じる |
| シートを close 方向に swipe | 幅の 30% 以上ドラッグで閉じる(タッチ / マウス両対応) |
シート本体を close 方向にポインタでドラッグ すると追従し、幅の 30% 以上ドラッグして放すと閉じます。閾値未満で放すと元の位置にスナップバックします。
| シート位置 | close 方向 | swipe 判定条件 |
|---|
right | 右へドラッグ | 縦より横が優位 + 右に 8px 以上 + 閾値超え |
left | 左へドラッグ | 縦より横が優位 + 左に 8px 以上 + 閾値超え |
- 縦方向の動きが優位な場合はスクロール意図と判断して swipe は engage しない(シート内 body のスクロールを邪魔しない)
- ボタン / リンク / フォーム要素から開始した swipe は無視(通常のクリック操作を阻害しない)
- swipe ジェスチャ直後の click 1 回は抑制される(snap back 時に backdrop 判定で誤って閉じない)
prefers-reduced-motion: reduce の環境では transition が無効化されますが、swipe そのものは機能します
- マウス / タッチ / ペン何でも Pointer Events 経由で動作
<ui-sheet> は Light DOM の Web Component ですが、ユーザーアクションによって表示される性質上、JS upgrade 前は 完全に非表示 にしています。
<button type="button" data-ui-sheet-open="my-sheet">開く</button>
<ui-sheet class="ui:sheet" id="my-sheet" data-side="right" aria-labelledby="my-sheet-title">
<header class="ui:sheet__header">
<h2 id="my-sheet-title">タイトル</h2>
<button type="button" data-ui-sheet-close aria-label="閉じる">×</button>
<div class="ui:sheet__body">
<footer class="ui:sheet__footer">
<button type="button" data-ui-sheet-close>キャンセル</button>
<button type="submit">保存</button>
- ルート:
<ui-sheet class="ui:sheet" id="..."> カスタム要素 (id 必須、トリガーが参照する)
- ラベル:
aria-labelledby / aria-label を <ui-sheet> に付与すれば内部 <dialog> に転写されます
- 補足説明 (任意):
aria-describedby も <ui-sheet> に付与すれば内部 <dialog> に転写されます
- 推奨子要素:
.ui:sheet__header / .ui:sheet__body / .ui:sheet__footer (任意、共通スタイル用)
<ui-sheet class="ui:sheet" id="my-sheet" data-side="right" aria-labelledby="my-sheet-title" data-ui-sheet-ready>
<!-- JS が動的に挿入。著者の children は全てこの中に移動される -->
<dialog aria-labelledby="my-sheet-title">
<header class="ui:sheet__header">...</header>
<div class="ui:sheet__body">...</div>
<footer class="ui:sheet__footer">...</footer>
| 要件 | 対応 |
|---|
role="dialog" / aria-modal | <dialog>.showModal() がネイティブ適用 |
| ラベル | 著者が <ui-sheet aria-labelledby="..."> 等を指定 (必須推奨) |
| フォーカストラップ | ネイティブ + Tab/Shift+Tab 自前 trap (保険) |
| 開く前の要素へのフォーカス復帰 | ネイティブ |
| Escape Esc Escape Esc 閉じ | ネイティブ |
| 背景スクロール抑止 | <html> の overflow:hidden を退避 / 復元 (modal と共通) |
import type { UiSheetElement } from "@packages/ui/sheet";
const $sheet = document.querySelector<UiSheetElement>("#my-sheet");
console.log($sheet?.isOpen);
| イベント | 発火元 | cancelable | bubbles | detail |
|---|
ui-sheet:beforeopen | <ui-sheet> | ✅ | ✅ | { $trigger: HTMLElement | null } |
ui-sheet:open | <ui-sheet> | ❌ | ✅ | { $trigger: HTMLElement | null } |
ui-sheet:close | <ui-sheet> | ❌ | ✅ | { returnValue: string } |
import type { SheetOpenDetail, SheetCloseDetail } from "@packages/ui/sheet";
document.addEventListener("ui-sheet:beforeopen", (e) => {
if (hasUnsavedChanges()) {
document.addEventListener("ui-sheet:close", (e) => {
console.log("closed:", e.detail.returnValue);