# Slash Command Menu

> Inline “/” command menu anchored to the caret of a textarea, with filtering, groups, icons, descriptions and keyboard navigation.

- **Slug:** `slash-command-menu` · **Category:** input · **Status:** beta
- **Import:** `import { CaretRect, SlashCommand, SlashCommandMenu, SlashCommandSelectDetails } from "@kura-ui/react/slash-command-menu";`
- **Docs:** https://kuro.abstergo.fyi/components/slash-command-menu
- **Registry item:** https://kuro.abstergo.fyi/r/slash-command-menu.json
- **Tags:** slash, commands, autocomplete, mention, caret, menu, composer, textarea

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/slash-command-menu.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add slash-command-menu
# npm package
npm install @kura-ui/react
```

## When to use

- 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).

## When not to use

- 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 | Required | Description |
| --- | --- | --- | --- | --- |
| `textareaRef` | `React.RefObject<HTMLTextAreaElement \| HTMLInputElement \| null>` | — | yes | Ref to the field to watch, e.g. the `ref` you pass to ComposerInput. The menu never takes focus from it. |
| `commands` | `readonly SlashCommand[]` | — | yes | Commands: { id, label, description?, icon?, group?, keywords?, shortcut?, insert?, disabled? }. |
| `onSelect` | `(command: SlashCommand, details: SlashCommandSelectDetails) => void` | — | no | Called after the typed “/query” has been replaced by `command.insert` (or removed). details.query is what was typed. |
| `onOpenChange` | `(open: boolean) => void` | — | no | Called when the menu opens or closes. |
| `trigger` | `string` | `"/"` | no | Trigger character. Only fires at the start of a line or after whitespace. |
| `side` | `"auto" \| "top" \| "bottom"` | `"auto"` | no | auto opens below the caret and flips above when there is no room. |
| `emptyMessage` | `string \| null` | `"No commands"` | no | Shown when nothing matches; null closes the menu instead. |
| `disabled` | `boolean` | `false` | no | Stop watching the field without unmounting. |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **SlashCommandMenu** (`data-slot="slash-command-menu"`) — Floating panel portalled to <body>, position: fixed at the trigger character. data-side = top | bottom; scales from the caret corner.
- **SlashCommandMenuGroup** (`data-slot="slash-command-menu-group"`) — role=group per `group`, labelled by its heading.
- **SlashCommandMenuItem** (`data-slot="slash-command-menu-item"`) — role=option row: icon tile, label (matched text emphasised), description, shortcut. data-active, data-disabled.
- **SlashCommandMenuFooter** (`data-slot="slash-command-menu-footer"`) — Key hints: ↑↓ navigate, ↵ insert, Esc dismiss.

## 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.

## Examples

### On a plain textarea (minimal)

```tsx
import {
  AlignLeft,
  Bug,
  Code,
  Eraser,
  Globe,
  Languages,
  ListTodo,
  MessageSquarePlus,
  PenLine,
  TestTube,
} from "lucide-react";
import * as React from "react";
import { type SlashCommand, SlashCommandMenu } from "@kura-ui/react/slash-command-menu";

const basic: SlashCommand[] = [
  { id: "summarize", label: "Summarize", insert: "Summarize the following:\n" },
  { id: "translate", label: "Translate", insert: "Translate into " },
  { id: "explain", label: "Explain", insert: "Explain like I'm new to this: " },
];

const commands: SlashCommand[] = [
  {
    id: "summarize",
    label: "Summarize",
    description: "Condense text into key points",
    icon: <AlignLeft />,
    group: "Write",
    keywords: ["tldr", "shorten"],
    insert: "Summarize the following in 5 bullet points:\n",
  },
  {
    id: "rewrite",
    label: "Rewrite",
    description: "Change tone, length or clarity",
    icon: <PenLine />,
    group: "Write",
    insert: "Rewrite this to be clearer and more concise:\n",
  },
  {
    id: "translate",
    label: "Translate",
    description: "Into any language",
    icon: <Languages />,
    group: "Write",
    insert: "Translate into ",
  },
  {
    id: "explain",
    label: "Explain code",
    description: "Walk through what a snippet does",
    icon: <Code />,
    group: "Code",
    insert: "Explain this code step by step:\n```\n\n```",
  },
  {
    id: "debug",
    label: "Debug",
    description: "Find the cause of an error",
    icon: <Bug />,
    group: "Code",
    keywords: ["fix", "error", "stack trace"],
    insert: "Here is an error. Find the root cause:\n",
  },
  {
    id: "tests",
    label: "Write tests",
    description: "Unit tests for the selected function",
    icon: <TestTube />,
    group: "Code",
    insert: "Write Vitest unit tests for:\n",
  },
  {
    id: "plan",
    label: "Plan",
    description: "Break a goal into steps first",
    icon: <ListTodo />,
    group: "Agent",
    insert: "Before doing anything, write a step-by-step plan for: ",
  },
  {
    id: "search",
    label: "Search the web",
    description: "Ground the answer in fresh sources",
    icon: <Globe />,
    group: "Agent",
    disabled: true,
  },
  {
    id: "new",
    label: "New chat",
    icon: <MessageSquarePlus />,
    group: "Session",
    shortcut: ["mod", "shift", "o"],
  },
  { id: "clear", label: "Clear input", icon: <Eraser />, group: "Session" },
];

export function SlashCommandMenuMinimal() {
  const ref = React.useRef<HTMLTextAreaElement>(null);
  return (
    <div className="w-full max-w-md">
      <textarea
        ref={ref}
        aria-label="Prompt"
        placeholder="Type / for commands"
        rows={3}
        className="w-full resize-none rounded-kura-lg bg-kura-surface px-3 py-2.5 text-sm shadow-kura-sm outline-none placeholder:text-kura-fg-subtle focus-visible:shadow-kura-focus"
      />
      <SlashCommandMenu textareaRef={ref} commands={basic} />
    </div>
  );
}
```

### Groups, icons, shortcuts and actions (full)

Ten commands in four groups, a disabled command, keywords (/tldr finds Summarize) and an action that clears the input.

```tsx
"use client";

import {
  AlignLeft,
  Bug,
  Code,
  Eraser,
  Globe,
  Languages,
  ListTodo,
  MessageSquarePlus,
  PenLine,
  TestTube,
} from "lucide-react";
import * as React from "react";
import { type SlashCommand, SlashCommandMenu } from "@kura-ui/react/slash-command-menu";

const commands: SlashCommand[] = [
  {
    id: "summarize",
    label: "Summarize",
    description: "Condense text into key points",
    icon: <AlignLeft />,
    group: "Write",
    keywords: ["tldr", "shorten"],
    insert: "Summarize the following in 5 bullet points:\n",
  },
  {
    id: "rewrite",
    label: "Rewrite",
    description: "Change tone, length or clarity",
    icon: <PenLine />,
    group: "Write",
    insert: "Rewrite this to be clearer and more concise:\n",
  },
  {
    id: "translate",
    label: "Translate",
    description: "Into any language",
    icon: <Languages />,
    group: "Write",
    insert: "Translate into ",
  },
  {
    id: "explain",
    label: "Explain code",
    description: "Walk through what a snippet does",
    icon: <Code />,
    group: "Code",
    insert: "Explain this code step by step:\n```\n\n```",
  },
  {
    id: "debug",
    label: "Debug",
    description: "Find the cause of an error",
    icon: <Bug />,
    group: "Code",
    keywords: ["fix", "error", "stack trace"],
    insert: "Here is an error. Find the root cause:\n",
  },
  {
    id: "tests",
    label: "Write tests",
    description: "Unit tests for the selected function",
    icon: <TestTube />,
    group: "Code",
    insert: "Write Vitest unit tests for:\n",
  },
  {
    id: "plan",
    label: "Plan",
    description: "Break a goal into steps first",
    icon: <ListTodo />,
    group: "Agent",
    insert: "Before doing anything, write a step-by-step plan for: ",
  },
  {
    id: "search",
    label: "Search the web",
    description: "Ground the answer in fresh sources",
    icon: <Globe />,
    group: "Agent",
    disabled: true,
  },
  {
    id: "new",
    label: "New chat",
    icon: <MessageSquarePlus />,
    group: "Session",
    shortcut: ["mod", "shift", "o"],
  },
  { id: "clear", label: "Clear input", icon: <Eraser />, group: "Session" },
];

export function SlashCommandMenuFull() {
  const ref = React.useRef<HTMLTextAreaElement>(null);
  const [value, setValue] = React.useState("");
  const [log, setLog] = React.useState("Type / at the start of a line");
  return (
    <div className="flex w-full max-w-md flex-col gap-2">
      <textarea
        ref={ref}
        aria-label="Prompt"
        value={value}
        onChange={(e) => setValue(e.currentTarget.value)}
        placeholder="Write a message… try /sum or /debug"
        rows={4}
        className="w-full resize-none rounded-kura-lg bg-kura-surface px-3 py-2.5 text-sm leading-6 shadow-kura-sm outline-none placeholder:text-kura-fg-subtle focus-visible:shadow-kura-focus"
      />
      <SlashCommandMenu
        textareaRef={ref}
        commands={commands}
        trigger="/"
        side="auto"
        emptyMessage="No matching command"
        onOpenChange={(open) => setLog(open ? "Menu open" : "Menu closed")}
        onSelect={(command, { query }) => {
          if (command.id === "clear") setValue("");
          setLog(`Ran /${command.label}${query ? ` (typed “/${query}”)` : ""}`);
        }}
      />
      <p className="px-1 text-kura-fg-subtle text-xs" aria-live="polite">
        {log}
      </p>
    </div>
  );
}
```

### Inside a composer (composition)

Pass the ComposerInput ref; Enter inserts the command instead of sending while the menu is open.

```tsx
"use client";

import {
  AlignLeft,
  Bug,
  Code,
  Eraser,
  Globe,
  Languages,
  ListTodo,
  MessageSquarePlus,
  PenLine,
  TestTube,
} from "lucide-react";
import * as React from "react";
import {
  Composer,
  ComposerActions,
  ComposerAttachButton,
  ComposerHint,
  ComposerInput,
  ComposerSubmit,
  ComposerToolbar,
  ComposerTools,
} from "@kura-ui/react/composer";
import { KbdShortcut } from "@kura-ui/react/kbd-shortcut";
import { type SlashCommand, SlashCommandMenu } from "@kura-ui/react/slash-command-menu";

const commands: SlashCommand[] = [
  {
    id: "summarize",
    label: "Summarize",
    description: "Condense text into key points",
    icon: <AlignLeft />,
    group: "Write",
    keywords: ["tldr", "shorten"],
    insert: "Summarize the following in 5 bullet points:\n",
  },
  {
    id: "rewrite",
    label: "Rewrite",
    description: "Change tone, length or clarity",
    icon: <PenLine />,
    group: "Write",
    insert: "Rewrite this to be clearer and more concise:\n",
  },
  {
    id: "translate",
    label: "Translate",
    description: "Into any language",
    icon: <Languages />,
    group: "Write",
    insert: "Translate into ",
  },
  {
    id: "explain",
    label: "Explain code",
    description: "Walk through what a snippet does",
    icon: <Code />,
    group: "Code",
    insert: "Explain this code step by step:\n```\n\n```",
  },
  {
    id: "debug",
    label: "Debug",
    description: "Find the cause of an error",
    icon: <Bug />,
    group: "Code",
    keywords: ["fix", "error", "stack trace"],
    insert: "Here is an error. Find the root cause:\n",
  },
  {
    id: "tests",
    label: "Write tests",
    description: "Unit tests for the selected function",
    icon: <TestTube />,
    group: "Code",
    insert: "Write Vitest unit tests for:\n",
  },
  {
    id: "plan",
    label: "Plan",
    description: "Break a goal into steps first",
    icon: <ListTodo />,
    group: "Agent",
    insert: "Before doing anything, write a step-by-step plan for: ",
  },
  {
    id: "search",
    label: "Search the web",
    description: "Ground the answer in fresh sources",
    icon: <Globe />,
    group: "Agent",
    disabled: true,
  },
  {
    id: "new",
    label: "New chat",
    icon: <MessageSquarePlus />,
    group: "Session",
    shortcut: ["mod", "shift", "o"],
  },
  { id: "clear", label: "Clear input", icon: <Eraser />, group: "Session" },
];

export function SlashCommandMenuInComposer() {
  const ref = React.useRef<HTMLTextAreaElement>(null);
  const [value, setValue] = React.useState("");
  return (
    <div className="flex w-full max-w-xl flex-col gap-2">
      <Composer value={value} onValueChange={setValue} onSubmit={() => {}}>
        <ComposerInput ref={ref} placeholder="Ask anything, or type / for commands" />
        <ComposerToolbar>
          <ComposerTools>
            <ComposerAttachButton />
          </ComposerTools>
          <ComposerActions>
            <ComposerHint />
            <ComposerSubmit />
          </ComposerActions>
        </ComposerToolbar>
      </Composer>
      <SlashCommandMenu
        textareaRef={ref}
        commands={commands}
        side="top"
        onSelect={(command) => {
          if (command.id === "clear") setValue("");
        }}
      />
      <div className="flex gap-3 px-1">
        <KbdShortcut keys={["/"]} label="Commands" variant="ghost" />
        <KbdShortcut keys={["enter"]} label="Send" variant="ghost" />
      </div>
    </div>
  );
}
```

## Dependencies

- npm: none
- Kura registry items: `kura-theme`, `kbd`, `kura-keys`, `utils`
- Related: `composer`, `command-palette`, `prompt-suggestions`
