コンテンツにスキップ

ui:sheet

画面端からスライドして出る panel コンポーネントです。

  • Web Component (<ui-sheet>) として提供しています。
  • WAI-ARIA Authoring Practices の Dialog (Modal) Pattern に準拠しています。
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>
</header>
<div class="ui:sheet__body">
<p>各種設定をここで変更できます。</p>
</div>
<footer class="ui:sheet__footer">
<button type="button" data-ui-sheet-close>キャンセル</button>
<button type="button">保存</button>
</footer>
</ui-sheet>
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>
</header>
<div class="ui:sheet__body">
<p>ナビゲーションメニュー等の用途。</p>
</div>
</ui-sheet>
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>
</header>
<div class="ui:sheet__body">
<p>幅は CSS 変数 <code>--ui-sheet-size</code> で個別に上書き可能。</p>
</div>
</ui-sheet>
属性 / CSS変数要素効果
data-sideroot"right" (既定) / "left"スライドする方向
--ui-sheet-sizerootCSS の <length>幅。既定 min(28rem, 100% - 2rem) を上書き可能
属性要素効果
data-ui-sheet-open任意対象 sheet の idクリックで対応する <ui-sheet> を開く
data-ui-sheet-close任意-<ui-sheet> 内のクリックでそのシートを閉じる
操作動作
Escape Escape シートを閉じる
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>
</header>
<div class="ui:sheet__body">
<p>本文...</p>
</div>
<footer class="ui:sheet__footer">
<button type="button" data-ui-sheet-close>キャンセル</button>
<button type="submit">保存</button>
</footer>
</ui-sheet>
  • ルート: <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>
</dialog>
</ui-sheet>
要件対応
role="dialog" / aria-modal<dialog>.showModal() がネイティブ適用
ラベル著者が <ui-sheet aria-labelledby="..."> 等を指定 (必須推奨)
フォーカストラップネイティブ + Tab/Shift+Tab 自前 trap (保険)
開く前の要素へのフォーカス復帰ネイティブ
Escape Escape 閉じネイティブ
背景スクロール抑止<html>overflow:hidden を退避 / 復元 (modal と共通)
import type { UiSheetElement } from "@packages/ui/sheet";
const $sheet = document.querySelector<UiSheetElement>("#my-sheet");
$sheet?.open();
$sheet?.close();
$sheet?.close("saved");
console.log($sheet?.isOpen);
イベント発火元cancelablebubblesdetail
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()) {
e.preventDefault();
}
});
document.addEventListener("ui-sheet:close", (e) => {
console.log("closed:", e.detail.returnValue);
});