# Mentions
URL: /docs/guides/mentions
Let users @-mention tools or custom items in the composer to guide the LLM.
Mentions let users type `@` in the composer to open a popover picker, select an item (e.g. a tool), and insert a directive into the message text. The LLM can then use the directive as a hint.
How It Works \[#how-it-works]
```
User types "@" → Trigger detected → Adapter provides categories/items
↓
Directive inserted ← User selects item from popover
↓
Message sent with ":tool[Label]{name=id}" in text
```
The mention system has three layers:
1. **Trigger detection** — watches the composer text for a trigger character (`@` by default) and extracts the query
2. **Adapter** — provides the categories and items to display in the popover (e.g. registered tools)
3. **Formatter** — serializes a selected item into directive text (`:type[label]{name=id}`) and parses it back for rendering
Quick Start \[#quick-start]
The fastest path is the pre-built [Mention UI component](/docs/ui/mention), which wires everything together with a single shadcn component:
```bash
npx shadcn@latest add "https://r.assistant-ui.com/composer-mention"
```
See the [Mention UI guide](/docs/ui/mention) for setup steps.
The rest of this guide covers the underlying concepts and customization points.
Mention Adapter \[#mention-adapter]
A `Unstable_MentionAdapter` provides the data for the popover. All methods are **synchronous** — use external state management (React Query, SWR, local state) for async data, then expose loaded results through the adapter.
```ts
import type { Unstable_MentionAdapter } from "@assistant-ui/core";
const myAdapter: Unstable_MentionAdapter = {
categories() {
return [
{ id: "tools", label: "Tools" },
{ id: "users", label: "Users" },
];
},
categoryItems(categoryId) {
if (categoryId === "tools") {
return [
{ id: "search", type: "tool", label: "Search" },
{ id: "calculator", type: "tool", label: "Calculator" },
];
}
if (categoryId === "users") {
return [
{ id: "alice", type: "user", label: "Alice" },
{ id: "bob", type: "user", label: "Bob" },
];
}
return [];
},
// Optional — global search across all categories
search(query) {
const lower = query.toLowerCase();
const all = [
...this.categoryItems("tools"),
...this.categoryItems("users"),
];
return all.filter(
(item) =>
item.label.toLowerCase().includes(lower) ||
item.id.toLowerCase().includes(lower),
);
},
};
```
Pass the adapter to `MentionRoot`:
```tsx
```
Built-in Tool Adapter \[#built-in-tool-adapter]
For the common case of mentioning registered tools, use `unstable_useToolMentionAdapter`:
```tsx
import { unstable_useToolMentionAdapter } from "@assistant-ui/react";
const adapter = unstable_useToolMentionAdapter({
// Format tool names for display (default: raw name)
formatLabel: (name) =>
name.replaceAll("_", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
// Custom category label (default: "Tools")
categoryLabel: "Tools",
// Explicit tool list (overrides model context tools)
// tools: [{ id: "search", type: "tool", label: "Search" }],
// Include model context tools alongside explicit tools
// includeModelContextTools: true,
});
```
The adapter automatically reads tools from the model context (registered via `Tools()` or `useAssistantTool`). When `tools` is provided, model context tools are excluded unless `includeModelContextTools` is set to `true`.
Directive Format \[#directive-format]
When a user selects a mention item, it is serialized into the composer text as a **directive**. The default format is:
```
:type[label]{name=id}
```
For example, selecting a tool named "get\_weather" with label "Get Weather" produces:
```
:tool[Get Weather]{name=get_weather}
```
When `id` equals `label`, the `{name=…}` attribute is omitted for brevity:
```
:tool[search]
```
Custom Formatter \[#custom-formatter]
Implement `Unstable_DirectiveFormatter` to use a different format:
```ts
import type { Unstable_DirectiveFormatter } from "@assistant-ui/core";
const slashFormatter: Unstable_DirectiveFormatter = {
serialize(item) {
return `/${item.id}`;
},
parse(text) {
const segments = [];
const re = /\/(\w+)/g;
let lastIndex = 0;
let match;
while ((match = re.exec(text)) !== null) {
if (match.index > lastIndex) {
segments.push({ kind: "text" as const, text: text.slice(lastIndex, match.index) });
}
segments.push({
kind: "mention" as const,
type: "tool",
label: match[1]!,
id: match[1]!,
});
lastIndex = re.lastIndex;
}
if (lastIndex < text.length) {
segments.push({ kind: "text" as const, text: text.slice(lastIndex) });
}
return segments;
},
};
```
Pass it to both the mention root and the message renderer:
```tsx
// Composer
...
// User messages
const SlashDirectiveText = createDirectiveText(slashFormatter);
```
Textarea vs Lexical \[#textarea-vs-lexical]
The mention system supports two input modes:
| | Textarea (default) | Lexical |
| ------------------------------- | ----------------------------------- | ---------------------------------------------------------- |
| **Input component** | `ComposerPrimitive.Input` | `LexicalComposerInput` |
| **Mention display in composer** | Raw directive text (`:tool[Label]`) | Inline chips (atomic nodes) |
| **Dependencies** | None | `@assistant-ui/react-lexical`, `lexical`, `@lexical/react` |
| **Best for** | Simple setups, minimal bundle | Rich editing, polished UX |
With **textarea**, selecting a mention inserts the directive string directly into the text. The user sees `:tool[Get Weather]{name=get_weather}` in the input.
With **Lexical**, selected mentions appear as styled inline chips that behave as atomic units — they can be selected, deleted, and undone as a whole. The underlying text still uses the directive format.
```tsx
import { LexicalComposerInput } from "@assistant-ui/react-lexical";
```
`LexicalComposerInput` auto-wires to the mention context — no extra props needed.
Rendering Mentions in Messages \[#rendering-mentions-in-messages]
Use `DirectiveText` as the `Text` component for user messages so directives render as inline chips instead of raw syntax:
```tsx
import { DirectiveText } from "@/components/assistant-ui/composer-mention";
```
For assistant messages, keep using your markdown renderer (e.g. `MarkdownText`) — the LLM typically does not emit directive syntax.
For a custom formatter, use `createDirectiveText`:
```tsx
import { createDirectiveText } from "@/components/assistant-ui/composer-mention";
const MyDirectiveText = createDirectiveText(myFormatter);
```
Processing Mentions on the Backend \[#processing-mentions-on-the-backend]
The message text arrives at your backend with directives inline. Parse them to extract mentioned items:
```ts
// Default format: :type[label]{name=id}
const DIRECTIVE_RE = /:([\w-]+)\[([^\]]+)\](?:\{name=([^}]+)\})?/g;
function parseMentions(text: string) {
const mentions = [];
let match;
while ((match = DIRECTIVE_RE.exec(text)) !== null) {
mentions.push({
type: match[1], // e.g. "tool"
label: match[2], // e.g. "Get Weather"
id: match[3] ?? match[2], // e.g. "get_weather"
});
}
return mentions;
}
// Example:
// parseMentions("Use :tool[Get Weather]{name=get_weather} to check")
// → [{ type: "tool", label: "Get Weather", id: "get_weather" }]
```
You can use the extracted mentions to:
* Force-enable specific tools for the LLM call
* Add context about mentioned users or documents to the system prompt
* Log which tools users request most often
Reading Mention State \[#reading-mention-state]
Use `unstable_useMentionContext` to programmatically access the mention popover state:
```tsx
import { unstable_useMentionContext } from "@assistant-ui/react";
function MyComponent() {
const mention = unstable_useMentionContext();
// mention.open — whether the popover is visible
// mention.query — current search text after "@"
// mention.categories — filtered category list
// mention.items — filtered item list
// mention.highlightedIndex — keyboard-navigated index
// mention.isSearchMode — true when global search is active
// mention.selectItem(item) — programmatically select an item
// mention.close() — close the popover
}
```
This hook must be used within a `ComposerPrimitive.Unstable_MentionRoot`.
Building a Custom Popover \[#building-a-custom-popover]
Use the mention primitives to build a fully custom popover:
```tsx
← Back
{(categories) =>
categories.map((cat) => (
{cat.label}
))
}
{(items) =>
items.map((item) => (
{item.label}
))
}
```
Primitives Reference \[#primitives-reference]
| Primitive | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------- |
| `Unstable_MentionRoot` | Provider — wraps the composer with trigger detection, keyboard navigation, and popover state |
| `Unstable_MentionPopover` | Container — only renders when a trigger is active (`role="listbox"`) |
| `Unstable_MentionCategories` | Render-function for the top-level category list |
| `Unstable_MentionCategoryItem` | Button that drills into a category (`role="option"`, auto `data-highlighted`) |
| `Unstable_MentionItems` | Render-function for items within the active category or search results |
| `Unstable_MentionItem` | Button that inserts a mention (`role="option"`, auto `data-highlighted`) |
| `Unstable_MentionBack` | Button that navigates back from items to categories |
See the [Composer API reference](/docs/api-reference/primitives/composer) for full prop details.
Related \[#related]
* [Mention UI Component](/docs/ui/mention) — pre-built shadcn component
* [Tools Guide](/docs/guides/tools) — register tools that appear in the mention picker
* [Composer Primitives](/docs/primitives/composer) — underlying composer primitives