Slash Command Menu
betaInline “/” command menu anchored to the caret of a textarea, with filtering, groups, icons, descriptions and keyboard navigation.
On a plain textarea
minimal
Groups, icons, shortcuts and actions
Ten commands in four groups, a disabled command, keywords (/tldr finds Summarize) and an action that clears the input.
full
Type / at the start of a line
Inside a composer
Pass the ComposerInput ref; Enter inserts the command instead of sending while the menu is open.
composition
Commands//SendEnter↵
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/slash-command-menu.jsonCopies the source into components/kura/slash-command-menu with its dependencies (kura-theme, kbd, kura-keys, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- Let users insert prompt templates or run actions by typing “/” in a composer (/summarize, /debug, /clear).
- Any textarea or input that needs caret-anchored suggestions triggered by a character (use `trigger` for “@” mentions or “#” tags).
Reach for something else when
- A global ⌘K launcher for navigation and actions — use command-palette.
- Choosing a model from a toolbar — use model-picker.
- Static clickable starters in an empty chat — use prompt-suggestions.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| textareaRef* | React.RefObject<HTMLTextAreaElement | HTMLInputElement | null> | — | Ref to the field to watch, e.g. the `ref` you pass to ComposerInput. The menu never takes focus from it. |
| commands* | readonly SlashCommand[] | — | Commands: { id, label, description?, icon?, group?, keywords?, shortcut?, insert?, disabled? }. |
| onSelect | (command: SlashCommand, details: SlashCommandSelectDetails) => void | — | Called after the typed “/query” has been replaced by `command.insert` (or removed). details.query is what was typed. |
| onOpenChange | (open: boolean) => void | — | Called when the menu opens or closes. |
| trigger | string | "/" | Trigger character. Only fires at the start of a line or after whitespace. |
| side | "auto" | "top" | "bottom" | "auto" | auto opens below the caret and flips above when there is no room. |
| emptyMessage | string | null | "No commands" | Shown when nothing matches; null closes the menu instead. |
| disabled | boolean | false | Stop watching the field without unmounting. |
All native attributes of the root element, className and ref pass through.
Anatomy
- SlashCommandMenuFloating panel portalled to <body>, position: fixed at the trigger character. data-side = top | bottom; scales from the caret corner.
data-slot="slash-command-menu" - SlashCommandMenuGrouprole=group per `group`, labelled by its heading.
data-slot="slash-command-menu-group" - SlashCommandMenuItemrole=option row: icon tile, label (matched text emphasised), description, shortcut. data-active, data-disabled.
data-slot="slash-command-menu-item" - SlashCommandMenuFooterKey hints: ↑↓ navigate, ↵ insert, Esc dismiss.
data-slot="slash-command-menu-footer"
Accessibility
- Role:
listbox - Focus never leaves the textarea. While open, the textarea gets aria-controls, aria-autocomplete=list and aria-activedescendant pointing at the highlighted option, so screen readers read the option as you arrow.
- A polite status announces the number of matching commands.
- Pointer selection uses mousedown preventDefault so the textarea keeps focus and its caret.
- Insertion uses the browser's insertText command where available, so ⌘Z undoes a command like normal typing.
- Reduced motion: The menu fades in without the scale and 2px travel.
| Keys | Action |
|---|---|
| / | Open (at line start or after a space) |
| ArrowDown / ArrowUp | Move the highlight (wraps, skips disabled) |
| Enter / Tab | Insert the highlighted command |
| Escape | Dismiss for this “/” (typing on does not reopen it) |
| Space / Backspace past / | Closes the menu |
Notes for agents
- Render the menu next to the field (anywhere in the tree); it portals itself to <body> and positions at the caret.
- It works with controlled fields: the replacement dispatches a native input event, so React onChange / Composer onValueChange fire.
- Use `insert` for prompt templates; omit it for actions and handle them in onSelect (e.g. id === "clear").
- Its keydown listener runs before React handlers and stops Enter/Tab/arrows while open, so Composer will not send mid-selection.
- getCaretRect(field, offset) is exported if you need caret coordinates for something else.