Command Dialog
The command palette in a modal dialog, summonable from anywhere.
Installation
Included in poetry-ui — available as
poetry_command_dialog
the moment you've installed Poetry,
with no per-component step. To own the source and edit it, copy it into your app:
bin/rails g poetry:add command-dialog
Default
<%= poetry_command_dialog(placeholder: "Type a command or search...") do |palette| %>
<% palette.with_trigger(variant: :outline) { "Open command palette" } %>
<% palette.with_group(heading: "Suggestions") do |group| %>
<% group.with_item(value: "calendar") { "Calendar" } %>
<% group.with_item(value: "emoji") { "Search Emoji" } %>
<% end %>
<% palette.with_separator %>
<% palette.with_group(heading: "Settings") do |group| %>
<% group.with_item(value: "profile", shortcut: "⌘P") { "Profile" } %>
<% group.with_item(value: "settings", shortcut: "⌘S") { "Settings" } %>
<% end %>
<% end %>
Empty state
<%= poetry_command_dialog(placeholder: "Search pages…") do |palette| %>
<% palette.with_trigger(variant: :outline) { "Search pages" } %>
<% palette.with_empty { "No pages found — try fewer letters." } %>
<% palette.with_item(value: "dashboard") { "Dashboard" } %>
<% palette.with_item(value: "reports") { "Reports" } %>
<% palette.with_item(value: "settings") { "Settings" } %>
<% end %>
Hotkey
<%= poetry_command_dialog(hotkey: "meta+j", placeholder: "Type a command or search…",
title: "Command menu") do |palette| %>
<% palette.with_trigger(variant: :outline) do %>
<span>Search…</span>
<kbd class="ml-2 rounded border bg-muted px-1.5 font-mono text-xs text-muted-foreground">⌘J</kbd>
<% end %>
<% palette.with_group(heading: "Quick actions") do |group| %>
<% group.with_item(value: "new-document", shortcut: "⌘N") { "New document" } %>
<% group.with_item(value: "invite-teammate", shortcut: "⌘I") { "Invite teammate" } %>
<% end %>
<% palette.with_separator %>
<% palette.with_group(heading: "Jump to") do |group| %>
<% group.with_item(value: "inbox") { "Inbox" } %>
<% group.with_item(value: "billing") { "Billing" } %>
<% group.with_item(value: "team-settings") { "Team settings" } %>
<% end %>
<% end %>
Scrollable
<%= poetry_command_dialog(placeholder: "Search a timezone…") do |palette| %>
<% palette.with_trigger(variant: :outline) { "Pick a timezone" } %>
<% palette.with_empty { "No timezone matches that search." } %>
<% palette.with_group(heading: "Americas") do |group| %>
<% group.with_item(value: "los-angeles") { "Los Angeles (GMT-8)" } %>
<% group.with_item(value: "denver") { "Denver (GMT-7)" } %>
<% group.with_item(value: "chicago") { "Chicago (GMT-6)" } %>
<% group.with_item(value: "new-york") { "New York (GMT-5)" } %>
<% group.with_item(value: "sao-paulo") { "São Paulo (GMT-3)" } %>
<% end %>
<% palette.with_group(heading: "Europe & Africa") do |group| %>
<% group.with_item(value: "london") { "London (GMT+0)" } %>
<% group.with_item(value: "paris") { "Paris (GMT+1)" } %>
<% group.with_item(value: "cairo") { "Cairo (GMT+2)" } %>
<% group.with_item(value: "moscow") { "Moscow (GMT+3)" } %>
<% group.with_item(value: "nairobi") { "Nairobi (GMT+3)" } %>
<% end %>
<% palette.with_group(heading: "Asia & Pacific") do |group| %>
<% group.with_item(value: "dubai") { "Dubai (GMT+4)" } %>
<% group.with_item(value: "mumbai") { "Mumbai (GMT+5:30)" } %>
<% group.with_item(value: "bangkok") { "Bangkok (GMT+7)" } %>
<% group.with_item(value: "singapore") { "Singapore (GMT+8)" } %>
<% group.with_item(value: "tokyo") { "Tokyo (GMT+9)" } %>
<% group.with_item(value: "sydney") { "Sydney (GMT+11)" } %>
<% group.with_item(value: "auckland") { "Auckland (GMT+13)" } %>
<% end %>
<% end %>
With icons
<%= poetry_command_dialog(placeholder: "Type a command or search…") do |palette| %>
<% palette.with_trigger(variant: :outline) { "Open command menu" } %>
<% palette.with_group(heading: "Suggestions") do |group| %>
<% group.with_item(value: "calendar", keywords: "date schedule") do %>
<%= poetry_icon(name: :calendar) %>
Calendar
<% end %>
<% group.with_item(value: "search-emoji", keywords: "reaction sticker") do %>
<%= poetry_icon(name: :smile) %>
Search emoji
<% end %>
<% group.with_item(value: "calculator", keywords: "math numbers") do %>
<%= poetry_icon(name: :calculator) %>
Calculator
<% end %>
<% end %>
<% palette.with_separator %>
<% palette.with_group(heading: "Account") do |group| %>
<% group.with_item(value: "profile", shortcut: "⌘P") do %>
<%= poetry_icon(name: :user) %>
Profile
<% end %>
<% group.with_item(value: "billing", shortcut: "⌘B") do %>
<%= poetry_icon(name: :"credit-card") %>
Billing
<% end %>
<% group.with_item(value: "settings", shortcut: "⌘S") do %>
<%= poetry_icon(name: :settings) %>
Settings
<% end %>
<% end %>
<% end %>
API
Poetry::Ui::Command::DialogComponent — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| description: | String | defaults to -> { I18n.t("poetry.command.dialog_description") } |
The sr-only description wired to aria-describedby (localized default). |
| dismissible: | Boolean | defaults to true |
Backdrop clicks close the palette; false keeps it open. |
| filter: | Boolean | defaults to true |
Passed through to the embedded Command: client-side filtering. |
| hotkey: | String | A global shortcut (\"meta+k\") that toggles the palette from anywhere; an accelerator, not the only way in. | |
| id: | String | Passed through: the embedded palette's base DOM id. | |
| list_label: | String | Passed through: the listbox's accessible name. | |
| loop: | Boolean | defaults to false |
Passed through: wraps arrow-key highlight movement at the ends. |
| placeholder: | String | Passed through: the filter input's placeholder text. | |
| show_close_button: | Boolean | defaults to -> { !dismissible } |
The close X, seated in the input row. Off by default while backdrop clicks close the palette (Esc, the backdrop, or picking an item all close it); on when dismissible: false so a pointer has a way out. Pass true to always show it. |
| title: | String | defaults to -> { I18n.t("poetry.command.dialog_title") } |
The dialog's sr-only accessible name (localized default) - override rather than remove. |
| value: | String | Passed through: seats the initial highlight on this item value. |
Slots
| Writer | Description |
|---|---|
| with_trigger | The trigger is a poetry Button wired to open - the Dialog pattern: with_trigger(variant: :outline) { \"Open palette\" }. |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=command-dialog] | Root wrapper around the trigger and the <dialog> - the palette's own chrome; the embedded Command inside carries its own part contract |
| [data-slot=dialog-content] | The <dialog> panel (Dialog's chrome retuned to overflow-hidden p-0) - positioning, animation, and the open state ride here |
| [data-slot=dialog-header] | Dialog's title block, sr-only here - the palette owns the visible surface |
| [data-slot=dialog-title] | The sr-only heading - the dialog's accessible name (defaults to the source string) |
| [data-slot=dialog-description] | The sr-only description wired to aria-describedby |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| dialog-content | data-open | panel is open (the dialog controller flips the pair at runtime) | — |
| dialog-content | data-closed | panel is closed or animating out (the server-rendered state) | — |
Wiring
The Stimulus surface each element carries — declared in the component, verified against rendered DOM in CI. Bare actions fire on the element's default event.
| Element | Controller | Wiring |
|---|---|---|
| root | poetry--core--dialog | registers · value dismissible · value hotkey (if) |
| content | poetry--core--dialog | close on cancel · backdropClose on click · target dialog |
| trigger | poetry--core--dialog | open |
| close | poetry--core--dialog | close |