Components Command

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

Petal Pro

Command

The ⌘K menu. Type to filter, arrow keys to move, Enter to run. Filtering happens in the PetalCommand hook so keystrokes never wait on the server, and every item is a real link or button - so navigate, patch and any phx-* binding work exactly as they do everywhere else in LiveView. The markup follows the WAI-ARIA combobox pattern, so it's keyboard and screen-reader friendly out of the box.
Inline palette

The inline shell is command/1 - a palette panel you can drop into a page, sidebar or picker. Start typing to filter the items; the highlight moves with the arrow keys and Enter runs the selection.

heex
The ⌘K dialog

command_dialog/1 is the same palette in a native <dialog>, so the top layer, focus trap, backdrop and Escape all come from the browser. Open it with the bound key (⌘K / Ctrl+K by default) or from any element with open_command/1. Selecting an item closes the dialog automatically. (This site already binds ⌘K to its own search, so the demo below opens from the button - pass shortcut="" to skip the global key when it's already taken.)

heex
Filtering and keywords

The hook matches the visible label plus any keywords you add, so "schedule" can find Calendar even though the word never appears on screen. Items are hidden, never reordered - the server owns DOM order, which keeps everything safe under LiveView patches. Pass loop to wrap the arrow keys from the last item back to the first.

Properties

command/1 and command_dialog/1 (the two shells):

elixir
          
  # command/1 - the inline palette
  attr :id, :string, required: true, doc: "unique id; the PetalCommand hook mounts here"
  attr :loop, :boolean, default: false, doc: "arrow keys wrap from the last item to the first"
  attr :class, :any, default: nil
  attr :rest, :global
  slot :inner_block, required: true

  # command_dialog/1 - the palette in a native <dialog>
  attr :id, :string, required: true
  attr :shortcut, :string, default: "k", doc: "key bound with Cmd/Ctrl to toggle; nil disables it"
  attr :loop, :boolean, default: false
  attr :reset_on_close, :boolean, default: true, doc: "clear the query when the dialog closes"
  attr :class, :any, default: nil
  attr :rest, :global
  slot :inner_block, required: true
  
        

The pieces inside: command_input/1, command_list/1 (with an accessible label), command_empty/1, command_group/1 (heading), command_separator/1, command_shortcut/1, and command_item/1:

elixir
          
  # command_item/1
  attr :value, :string, doc: "used for filtering; defaults to the item's text"
  attr :keywords, :list, default: [], doc: "extra search aliases for this item"
  attr :disabled, :boolean, default: false
  attr :navigate, :string, default: nil, doc: "live_redirect target - renders the item as a link"
  attr :patch, :string, default: nil, doc: "live_patch target - renders the item as a link"
  attr :rest, :global, doc: "e.g. phx-click, data-keep-open"
  slot :inner_block, required: true