| name | design-interactions |
| description | Design the interactive UX micro-patterns of a full-stack Rails 8.1 Hotwire app — modals/dialogs, drawers, dropdowns, tabs, toast/flash notifications, inline editing, loading/skeleton states, empty states, and infinite scroll — built with Turbo Frames/Streams and Stimulus. Menu-driven with a Recommended default per menu (native <dialog> + Turbo Frame + Stimulus for overlays, Stimulus auto-dismiss + Turbo Stream for toasts, Turbo loading states + skeletons for feedback). Owns the visual/UX shape of these patterns and the accessibility contract each must meet (the canonical a11y rules live in design-accessibility); the Turbo/Stimulus plumbing is rails-hotwire. Detects the installed UI kit and Hotwire wiring first, then verifies the interaction works and is keyboard/screen-reader accessible. Apply when building any of these patterns. Web apps only. |
| metadata | {"owner":"rails-design","status":"stable"} |
| user-invocable | true |
| argument-hint | [pattern] |
design-interactions
Purpose
Owns the interactive UX micro-patterns that make a Hotwire app feel responsive:
modals/dialogs, drawers, dropdowns, tabs, toast/flash notifications, inline editing,
loading/skeleton states, empty states, and infinite scroll. It owns the visual and UX
shape of each pattern — what it looks like, how it behaves, and the accessibility it
must meet — built on Turbo Frames/Streams and Stimulus. The plumbing (how Turbo
Streams render, how Stimulus controllers wire up, broadcasting) belongs to
../rails-hotwire/; this skill is the design layer on top, and it defers every a11y
rule to ../design-accessibility/.
When to Apply
Use this skill when the task is:
- A modal / dialog, drawer/off-canvas, dropdown menu, or tabs
- Toast / flash notifications (transient feedback)
- Inline editing (click-to-edit a field/row in place)
- Loading / skeleton states and optimistic feedback
- Empty states (zero-data, no-results, first-run)
- Infinite scroll / load-more for feeds and long lists
Do not use this skill when the task is:
- The Turbo/Stimulus mechanics themselves (frame targets, stream actions, controller lifecycle) → read
../rails-hotwire/SKILL.md
- The component primitives (button, menu, card markup) → read
../design-components/SKILL.md
- Form layout and validation/error UX → read
../design-forms/SKILL.md (inline editing here links there for the field UX)
- Navigation layout (the navbar/drawer placement) → read
../design-layout-navigation/SKILL.md (this skill owns the drawer's open/close behavior)
- Tokens / theming → read
../design-foundations/SKILL.md
- The full ARIA / focus / keyboard spec → read
../design-accessibility/SKILL.md
- An API-only app — no interactions to design
Detect Before You Generate
grep -nE "api_only" config/application.rb
bundle list 2>/dev/null | grep -E "turbo-rails|stimulus-rails"
ls app/javascript/controllers/ 2>/dev/null
grep -E "daisyui|flowbite" Gemfile.lock package.json 2>/dev/null
grep -rlnE "turbo_stream|turbo-frame|<dialog|data-controller" app/views 2>/dev/null | head
grep -rnE "flash\[|render.*flash|toast" app/views app/controllers 2>/dev/null | head
- Hotwire is required — these patterns assume
turbo-rails + stimulus-rails. If
missing, set them up via ../rails-hotwire/ first.
- Reuse existing Stimulus controllers instead of duplicating (a generic
dialog/dismiss/toggle controller usually already covers several patterns).
- If a UI kit ships the pattern (daisyUI modal/drawer/dropdown/tabs, Flowbite JS),
prefer its markup — but keep the a11y requirements from
../design-accessibility/.
Menu
Three menus via AskUserQuestion; defaults marked Recommended. Ask unless detected.
Overlay mechanism (modals / drawers)
| Option | One-line trade-off | Deep dive |
|---|
Native <dialog> + Turbo Frame + Stimulus (Recommended) | Browser-native focus trap, Esc, backdrop + inert; server-rendered content lazy-loaded into a frame. Least JS, best a11y. | modals-dialogs.md |
| UI-kit JS (daisyUI/Flowbite) | Prebuilt modal/drawer with its own toggle JS; fast, ties you to the kit's behavior/markup. | drawers-dropdowns-tabs.md |
| Custom Stimulus overlay | Hand-built role="dialog" overlay; full control, you own focus trap + Esc + scroll lock. | modals-dialogs.md |
Toast / flash notifications
| Option | One-line trade-off | Deep dive |
|---|
| Stimulus auto-dismiss + Turbo Stream (Recommended) | Flash renders into a live region, a Stimulus controller auto-dismisses; Turbo Stream can push toasts after actions/broadcasts. | toasts-flash.md |
| Static flash (no JS) | Server-rendered flash banner, dismissed on next navigation; simplest, no transient/stacked toasts. | toasts-flash.md |
Loading / feedback
| Option | One-line trade-off | Deep dive |
|---|
| Turbo loading states + skeletons (Recommended) | Drive aria-busy/[data-turbo-...] styling + skeleton placeholders that match final layout; no layout shift. | loading-skeleton-states.md |
| Spinners | A single spinner during requests; simple, but jumpier and less informative than skeletons. | loading-skeleton-states.md |
Decision Flow
- Overlays: prefer native
<dialog> — it gives focus trap, Esc, and backdrop
inert for free, which is most of the a11y work. Load contents lazily via a Turbo Frame.
Use the UI kit's modal/drawer when you're already all-in on it; reach for a custom
Stimulus overlay only when neither fits. Whatever you pick, the a11y contract (focus on
open, trap, return focus on close) comes from ../design-accessibility/.
- Toasts: Stimulus auto-dismiss + Turbo Stream is the flexible default — transient,
stackable, and pushable from server actions or broadcasts; render them into a persistent
live region so screen readers hear them. Use the static flash when you don't need
transient/stacked feedback.
- Loading: skeletons that mirror the final layout beat spinners — they prevent
layout shift and feel faster. Drive them off Turbo's
aria-busy. Use a spinner for
small, in-place actions.
- Inline editing swaps a display partial for a form via a Turbo Frame, and must manage
focus on enter/save/cancel (inline-editing.md); the field
UX itself is
../design-forms/.
- Empty states are a design surface, not an afterthought — every list/search/dashboard
needs a designed zero-state with a clear next action (empty-states.md).
- Infinite scroll suits feeds; keep a paginated fallback for findability/SEO and
announce appended content. Pagination choice is
../design-layout-navigation/.
Problem → Reference
| Task | Read |
|---|
Modal / dialog (native <dialog> or custom), lazy-loaded via Turbo Frame | references/modals-dialogs.md |
| Drawer/off-canvas, dropdown menu, tabs (kit or Stimulus) | references/drawers-dropdowns-tabs.md |
| Toast / flash notifications, auto-dismiss, live regions, Turbo Stream push | references/toasts-flash.md |
| Inline / click-to-edit a field or row in place | references/inline-editing.md |
| Loading, skeleton placeholders, optimistic/busy states | references/loading-skeleton-states.md |
| Empty states: zero-data, no-results, first-run | references/empty-states.md |
| Infinite scroll / load-more with Turbo Streams | references/infinite-scroll.md |
Verify
An interaction is "done" when it works, is keyboard-operable, and announces itself:
bin/rails server -d && sleep 3
curl -fsS localhost:3000/ -o /tmp/p.html
grep -qE "<dialog|role=\"dialog\"|data-controller" /tmp/p.html && echo "✓ interactive markup present"
grep -qE "aria-live|role=\"status\"|role=\"alert\"" /tmp/p.html && echo "✓ live region for toasts/updates"
kill "$(cat tmp/pids/server.pid)"
bin/rails test test/system 2>/dev/null || bundle exec rspec spec/system
Manually: open the overlay with the keyboard (focus moves in, Esc closes, focus returns),
fire a toast (it announces and auto-dismisses), and confirm skeletons don't shift layout.
Then route to ../design-accessibility/ for the focus/ARIA audit and ../design-visual-qa/
to lock the behavior with tests.