# Slash Commands URL: /docs/guides/slash-commands Let users type / in the composer to trigger predefined actions from a popover picker. Slash commands let users type `/` in the composer to open a popover, browse available commands, and execute one. Unlike [mentions](/docs/guides/mentions) (which insert text into the message), slash commands trigger **actions** — the `/command` text is removed from the composer and a callback fires. How It Works \[#how-it-works] ``` User types "/" → Trigger detected → Adapter provides commands ↓ Command executed ← User selects command from popover ↓ "/command" text removed from composer ``` The slash command system is built on the same [trigger popover architecture](#trigger-popover-architecture) as mentions. It has two layers: 1. **Adapter** — provides the list of available commands (flat list by default, or categorized for advanced use) 2. **SlashCommandRoot** — a convenience wrapper that pre-configures the trigger character (`/`) and action-based select behavior Quick Start \[#quick-start] 1\. Define Commands \[#1-define-commands] ```tsx import { unstable_useSlashCommandAdapter, ComposerPrimitive, } from "@assistant-ui/react"; // Define commands outside the component for a stable reference const COMMANDS = [ { name: "summarize", description: "Summarize the conversation", execute: () => console.log("Summarize!"), }, { name: "translate", description: "Translate text to another language", execute: () => console.log("Translate!"), }, { name: "help", description: "List all available commands", }, ]; function MyComposer() { const slashAdapter = unstable_useSlashCommandAdapter({ commands: COMMANDS, }); return ( Send {(items) => items.map((item, index) => ( {item.label} {item.description && {item.description}} )) } ); } ``` 2\. Handle Command Selection \[#2-handle-command-selection] There are two ways to handle command execution: **Via `execute` on each item** — define the action inline in the command definition: ```ts { name: "summarize", execute: () => runSummarize() } ``` **Via `onSelect` prop** — handle all commands in one place: ```tsx { switch (item.id) { case "summarize": runSummarize(); break; case "translate": runTranslate(); break; } }} > ``` Both `execute` and `onSelect` fire when a command is selected. Use whichever pattern fits your code. Custom Adapter \[#custom-adapter] The `unstable_useSlashCommandAdapter` hook uses a **flat list** — all commands show immediately when `/` is typed, with search filtering as the user types. This is the recommended UX for most cases. For **categorized navigation** (drill-down into groups), build the adapter manually. Return categories from `categories()` and items from `categoryItems()`. The popover will show categories first, then items within the selected category: ```ts import type { Unstable_SlashCommandAdapter } from "@assistant-ui/core"; const adapter: Unstable_SlashCommandAdapter = { categories() { return [ { id: "actions", label: "Actions" }, { id: "export", label: "Export" }, ]; }, categoryItems(categoryId) { if (categoryId === "actions") { return [ { id: "summarize", type: "command", label: "/summarize", description: "Summarize the conversation" }, { id: "translate", type: "command", label: "/translate", description: "Translate text" }, ]; } if (categoryId === "export") { return [ { id: "pdf", type: "command", label: "/export pdf", description: "Export as PDF" }, { id: "markdown", type: "command", label: "/export md", description: "Export as Markdown" }, ]; } return []; }, // Optional — enables search across all categories search(query) { const lower = query.toLowerCase(); const all = [...this.categoryItems("actions"), ...this.categoryItems("export")]; return all.filter( (item) => item.label.toLowerCase().includes(lower) || item.description?.toLowerCase().includes(lower), ); }, }; ``` When using a categorized adapter, add `TriggerPopoverCategories` to your popover UI: ```tsx ← Back {(categories) => categories.map((cat) => ( {cat.label} ))} {(items) => items.map((item, index) => ( {item.label} ))} ``` Combining with Mentions \[#combining-with-mentions] Slash commands and mentions can coexist on the same composer. Nest both roots — the [plugin protocol](#trigger-popover-architecture) ensures they don't conflict: ```tsx Send {/* Mention popover — shows when @ is typed */} {(categories) => categories.map((cat) => ( {cat.label} ))} {(items) => items.map((item) => ( {item.label} ))} {/* Slash command popover — shows when / is typed */} {(items) => items.map((item, index) => ( {item.label} ))} ``` Each root provides its own `TriggerPopoverContext`. When the user types `@`, the mention popover opens. When they type `/`, the slash command popover opens. Keyboard events route to whichever popover is active. Keyboard Navigation \[#keyboard-navigation] Same keyboard bindings as mentions: | Key | Action | | -------------------- | ------------------------------------------------- | | ArrowDown | Highlight next item | | ArrowUp | Highlight previous item | | Enter | Execute highlighted command / drill into category | | Escape | Close popover | | Backspace | Go back to categories (when query is empty) | Trigger Popover Architecture \[#trigger-popover-architecture] Both mentions and slash commands are built on a generic **trigger popover** system: * `ComposerPrimitive.Unstable_TriggerPopoverRoot` — the generic root, parameterized by trigger character and select behavior * `ComposerPrimitive.Unstable_MentionRoot` — preset with `trigger="@"` and `onSelect: insertDirective` * `ComposerPrimitive.Unstable_SlashCommandRoot` — preset with `trigger="/"` and `onSelect: action` The trigger popover primitives (`TriggerPopoverPopover`, `TriggerPopoverItems`, etc.) are shared across both. You can also use `TriggerPopoverRoot` directly to build custom trigger systems with other characters (e.g. `:` for emoji). ComposerInput Plugin Protocol \[#composerinput-plugin-protocol] Under the hood, each trigger root registers a **ComposerInputPlugin** with the composer input. This is a generic protocol that decouples the input from any specific trigger: ```ts type ComposerInputPlugin = { handleKeyDown(e: KeyboardEvent): boolean; setCursorPosition(pos: number): void; }; ``` The input iterates over registered plugins for keyboard events and cursor changes. This is what enables multiple trigger roots to coexist without conflict. Primitives Reference \[#primitives-reference] | Primitive | Description | | ------------------------------------- | --------------------------------------------------------------------------------- | | `Unstable_SlashCommandRoot` | Convenience wrapper — `TriggerPopoverRoot` with `trigger="/"` and action behavior | | `Unstable_TriggerPopoverRoot` | Generic root — configurable trigger character and select behavior | | `Unstable_TriggerPopoverPopover` | Container — only renders when a trigger is active (`role="listbox"`) | | `Unstable_TriggerPopoverCategories` | Render-function for the top-level category list | | `Unstable_TriggerPopoverCategoryItem` | Button that drills into a category (`role="option"`, auto `data-highlighted`) | | `Unstable_TriggerPopoverItems` | Render-function for items within the active category or search results | | `Unstable_TriggerPopoverItem` | Button that selects an item (`role="option"`, auto `data-highlighted`) | | `Unstable_TriggerPopoverBack` | Button that navigates back from items to categories | Related \[#related] * [Mentions Guide](/docs/guides/mentions) — `@`-mention system built on the same architecture * [Suggestions Guide](/docs/guides/suggestions) — static follow-up prompts (different from slash commands) * [Composer Primitives](/docs/primitives/composer) — underlying composer primitives