dialog-patterns について
dialog-patternsは、アクセシブルなダイアログやオーバーレイを作るRails開発者向けのスキルです。ネイティブHTML dialog、Turbo Frames、Stimulusを使う方法に加え、JavaScript不要の確認パターンも扱います。
主な機能
- 01ネイティブダイアログのアクセシビリティ
- 02JavaScript不要の確認ダイアログ
- 03Turbo Frameによる非同期モーダル
- 04トーストとスライドオーバーのパターン
- 05フォーム検証への対応
活用シーン
- 01Railsでモーダルフォームを構築
- 02破壊的操作の確認
- 03トースト通知の表示
- 04フィルター用サイドパネルの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill dialog-patternsインストール後、Claude Code で/dialog-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/dialog-examples.mdtoast-slideover-patterns.mdzero-js-patterns.md詳細
Rails 向けネイティブダイアログのパターン
ネイティブ HTML の <dialog> 要素を Turbo Frames および Stimulus と組み合わせて、アクセシブルでモダンなダイアログ UI を構築できます。JavaScript フレームワークや大規模なライブラリは不要です。
このスキルを使用する場面
- フォーム、確認、コンテンツ用のモーダルダイアログを構築するとき
- トースト通知やアラート通知を作成するとき
- 確認ダイアログ(削除、破壊的な操作)を実装するとき
- フォーカス管理とアクセシビリティが必要なオーバーレイ UI を実装するとき
ネイティブの <dialog> を使用する理由
| 機能 | ネイティブの <dialog> | カスタムモーダル |
|---|---|---|
| フォーカストラップ | 組み込み | 手動で実装 |
| ESC キーで閉じる | 組み込み | 手動で実装 |
| 背景 | 組み込み(::backdrop) | 手動でオーバーレイを実装 |
| アクセシビリティ | ネイティブの role="dialog" | ARIA を手動で実装 |
| 最前面レイヤー | 自動(すべてのコンテンツの上) | z-index の調整が必要 |
| スクロールロック | 自動 | overflow: hidden を手動で設定 |
JavaScript 不要の確認ダイアログ(推奨)
最新のブラウザーは、宣言的にダイアログを制御できる Invoker Commands API に対応しています。JavaScript は不要です。詳しい例は references/zero-js-patterns.md を参照してください。
クイックリファレンス
<%= button_tag "Delete", commandfor: "delete-#{post.id}", command: "show-modal" %>
<dialog id="delete-<%= post.id %>" closedby="any" role="alertdialog">
<h3>Delete "<%= post.title %>"?</h3>
<button commandfor="delete-<%= post.id %>" command="close">Cancel</button>
<%= button_to "Delete", post, method: :delete %>
</dialog>
主な属性
| 属性 | 用途 |
|---|---|
commandfor="id" | 制御するダイアログを指定します |
command="show-modal" | モーダルとして開きます(背景、フォーカストラップ) |
command="close" | ダイアログを閉じます |
closedby="any" | 背景クリックと ESC キーで閉じられるようにします |
JavaScript 不要の方法と Stimulus の使い分け
| シナリオ | 方法 |
|---|---|
| シンプルな確認 | JavaScript 不要(Invoker Commands) |
| 非同期コンテンツを含むモーダル | Stimulus + Turbo Frames |
| 複雑な複数ステップのダイアログ | Stimulus コントローラー |
| アニメーション | CSS @starting-style |
その他のパターン(references/ を参照)
@starting-styleを使用した、表示・非表示の切り替え用 CSS アニメーション- 見づらいブラウザーのダイアログを置き換える Turbo.config.forms.confirm
- クロスブラウザー互換性のための プログレッシブエンハンスメント
基本パターン:Turbo Frames を使った非同期モーダル
Rails のモーダルには、次の 3 つの技術を組み合わせるパターンを推奨します。
- Turbo Frame - ページを再読み込みせずに非同期でコンテンツを読み込みます
- ネイティブの
<dialog>- アクセシブルなモーダル表示を実現します - Stimulus コントローラー - ライフサイクルを管理します
ステップ 1:レイアウトコンテナー
レイアウトにモーダル用の turbo-frame を追加します。
<%# app/views/layouts/application.html.erb %>
<body>
<%= yield %>
<%# Modal injection point %>
<%= turbo_frame_tag :modal %>
</body>
ステップ 2:トリガーリンク
任意のリンクからモーダルフレームをターゲットにします。
<%# Any view %>
<%= link_to "New Post", new_post_path, data: { turbo_frame: :modal } %>
<%= link_to "Edit", edit_post_path(@post), data: { turbo_frame: :modal } %>
<%= link_to "Confirm Delete", confirm_delete_post_path(@post), data: { turbo_frame: :modal } %>
ステップ 3:モーダルコンテンツのビュー
モーダルコンテンツを、内側のフレームを含む一致する turbo-frame で囲みます。
<%# app/views/posts/new.html.erb %>
<%= turbo_frame_tag :modal do %>
<%# Inner frame prevents flash during form validation %>
<%= turbo_frame_tag :modal_content do %>
<dialog data-controller="dialog" data-action="click->dialog#clickOutside" open>
<article>
<header>
<h2>New Post</h2>
<button data-action="dialog#close" aria-label="Close">×</button>
</header>
<%= render "form", post: @post %>
</article>
</dialog>
<% end %>
<% end %>
ステップ 4:Stimulus コントローラー
主な動作:接続時に showModal() を呼び出し、切断時に replaceChildren() を呼び出して古いコンテンツを防ぎ、clickOutside で背景クリックによる閉じる操作に対応します。
Stimulus コントローラー全体、CSS のスタイル設定、Tailwind バリアントについては、references/dialog-examples.md を参照してください。
Turbo Frames をネストする理由
フレームをネストするパターン(modal > modal_content)を使うと、コンテンツのちらつきを防げます。
<%= turbo_frame_tag :modal do %>
<%= turbo_frame_tag :modal_content do %>
<dialog>...</dialog>
<% end %>
<% end %>
ネストしたフレームを使わない場合の問題: モーダル内のフォームでバリデーションエラーが発生して再描画されると、置き換え前に外側のフレームに古いコンテンツが一時的に表示されます。
ネストしたフレームを使う場合の解決策: 内側のフレームがフォームの再描画を個別に処理するため、モーダルの構造が安定します。
モーダル内のフォーム処理
送信成功時
Turbo でリダイレクトしてモーダルを閉じ、ページを更新します。
# app/controllers/posts_controller.rb
def create
@post = Post.new(post_params)
if @post.save
redirect_to posts_path, notice: "Post created!"
else
render :new, status: :unprocessable_entity
end
end
リダイレクトは _top(ページ全体)に移動し、実質的にモーダルを閉じます。
バリデーションエラー
422 ステータスでフォームを再描画し、モーダルを開いたままにします。
render :new, status: :unprocessable_entity
Turbo Stream レスポンス(モーダル内にとどまる)
turbo_stream.update("modal", "") を使用して、全体をリダイレクトせずにモーダルをクリアします。詳しい例は references/dialog-examples.md を参照してください。
確認ダイアログのパターン
破壊的な操作では、confirm_delete メンバールートを追加し、turbo frame 内にダイアログを描画して、data: { turbo_frame: :modal } を指定した link_to で起動します。
確認ダイアログのビュー、ルート、起動方法の全例については、references/dialog-examples.md を参照してください。
アラート/トーストのパターン
フラッシュメッセージや通知に使用します。モーダルではない表示には、showModal() ではなく show() を使用します。実装の全例については、references/toast-slideover-patterns.md を参照してください。
<dialog class="toast" data-controller="toast" data-toast-duration-value="5000">
<p><%= message %></p>
</dialog>
主な違いは、show() は背景やフォーカストラップなしで開き(トースト)、showModal() は背景付きで中央に表示される(モーダル)ことです。
スライドオーバーパネルのパターン
サイドパネル(設定、フィルター、詳細)に使用します。スタイルとアニメーションについては、references/toast-slideover-patterns.md を参照してください。
<dialog class="slideover" data-controller="dialog" data-action="click->dialog#clickOutside">
<aside>
<header><h2>Filters</h2></header>
<%= render "filters" %>
</aside>
</dialog>
アクセシビリティ
ネイティブの <dialog> は、フォーカストラップ、ESC キーでのクローズ、背景の非アクティブ化、トップレイヤーへの配置を自動で提供します。さらに、以下も確認してください。
- 常に表示される閉じるボタンを設置する(ESC キーだけに頼らない)
- 説明を補うコンテキストとして
aria-labelledby/aria-describedbyを設定する - 閉じたときにフォーカスを起動要素に戻す(
connect()でdocument.activeElementを保存する)
アクセシビリティの強化とフォーカスを戻す例については、references/dialog-examples.md を参照してください。
よく使うパターンの概要
| パターン | コンテナー | Stimulus | show メソッド |
|---|---|---|---|
| モーダルフォーム | turbo_frame_tag :modal | dialog | showModal() |
| 確認 | turbo_frame_tag :modal | dialog | showModal() |
| トースト/アラート | 固定位置 | toast | show() |
| スライドオーバー | turbo_frame_tag :modal | dialog | showModal() |
避けるべきアンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
<dialog> を使わないカスタムモーダル | ネイティブのアクセシビリティがない | ネイティブの <dialog> を使用する |
| ネストした turbo-frame がない | バリデーション時にコンテンツがちらつく | 内側の frame を追加する |
| 閉じるときに frame をクリアしない | 再表示時に古いコンテンツが残る | disconnect() で replaceChildren() を使ってクリアする |
| 重なり順に z-index を使う | 他の要素との競合を招く | <dialog> はトップレイヤーを使用する |
| フォーカストラップを手動で実装する | 複雑で、エラーが起きやすい | showModal() に任せる |
| インラインの backdrop div | マークアップが増える | ::backdrop 疑似要素を使用する |
ダイアログのテスト
# System test - use `within "dialog"` to scope assertions
within "dialog" do
fill_in "Title", with: "My Post"
click_button "Create"
end
expect(page).not_to have_selector("dialog[open]") # Modal closed
ブラウザーのサポート状況
| パターン | Chrome | Firefox | Safari |
|---|---|---|---|
ネイティブの <dialog> | 37+ | 98+ | 15.4+ |
| Invoker Commands | 135+ | 144+ | 26.2+ |
@starting-style | 117+ | 129+ | 17.5+ |
古いブラウザー向け:dialog polyfill、invokers polyfill。プログレッシブエンハンスメントの方法については、references/zero-js-patterns.md を参照してください。