import { MentionInput } from "@elabs-ai/components-ui";npm install @elabs-ai/components-tokens @elabs-ai/components-uiPaste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.
Add the brand-ui MentionInput component (from @elabs-ai/components-ui) to this project. 1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`. 2. Look MentionInput up before writing code: `npx -y @elabs-ai/components-cli docs MentionInput` prints its real props, variants, what it pairs with and what to avoid. 3. If @elabs-ai/components-ui is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`. 4. Use it here: [the screen or file, and what it should show] 5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop. 6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.
Examples
Typing
Ordinary prose already in the field, with the popup closed.
Popup Open
The popup open on a partial query, driven entirely from the keyboard. The play function types the trigger and arrows down, then asserts the field is still the focused element and that `aria-activedescendant` names the option carrying `aria-selected="true"`.
With Chips
Two inserted mentions. Each `@Name` run is painted by an `aria-hidden` mirror layer sitting over the field, so it reads as a chip while the text underneath stays ordinary, selectable, spellcheckable textarea content. The play function proves atomicity: one Backspace beside a mention removes the entire token.
Serialized Output
What a consumer actually sends. `serializeMentions()` returns `{ text, mentionedIds }` with the ids **deduped** and in **document order**.
Async Roster
An app-side roster. The consumer passes `filter={() => true}` (the popup shows whatever it is given) and fetches from `onQueryChange`. There is deliberately no `loading` prop — render whatever placeholder the surface wants inside `MentionInputEmpty`.
Custom Trigger
A non-default trigger — the same machinery drives `#`-style references.
Mirror Tracks Field Resize
**Regression lock — the mirror follows a resize of the field.** Narrows an ancestor that sits OUTSIDE the `MentionInput` root, so neither the field's own attributes nor the document root's change — the `MutationObserver` arms are structurally blind to it and only the `ResizeObserver` can keep the overlay's box on top of the field's.
Mirror Tracks Field Style Change
**Regression lock — the mirror follows a change to the FIELD's own style.** A third, font-free lever: `letter-spacing` changes glyph advance while the pinned box stays put (the play function asserts the box did not move) and no font loads. It was written to isolate the `MutationObserver` arm — and it does not: measured, it still passes with that observer deleted, so `ResizeObserver` reacts here too. Kept because it is a third independent scenario in the all-arms lock, and because a consumer restyling the field is a real case.
API
MentionInput
| Prop | Type | Description |
|---|---|---|
optionsrequired | MentionOption[] | The roster the popup offers. |
value | MentionValue | Controlled value. |
defaultValue | MentionValue | Uncontrolled initial value. |
onValueChange | (value: MentionValue) => void | Called with the whole value whenever text or mentions change. |
trigger | string | Trigger character. Default `"@"`. |
filter | (option: MentionOption, query: string) => boolean | Which options survive the current query. Defaults to `defaultMentionFilter`; pass `() => true` when the roster is fetched app-side from `onQueryChange`. |
onQueryChange | (query: string | null) => void | The query changed. `null` means the popup closed. |
open | boolean | Controlled popup state. |
onOpenChange | (open: boolean) => void | Called when the popup wants to open or close. |
childrenrequired | ReactNode |
MentionInputContent
MentionInputEmpty
MentionInputItem
| Prop | Type | Description |
|---|---|---|
optionrequired | MentionOption | |
children | ReactNode |
MentionInputList
| Prop | Type | Description |
|---|---|---|
children | ReactNode | ((option: MentionOption, index: number) => ReactNode) | Either static nodes, or a render function called once per filtered option. The function form is the one to reach for — the parent has to hand each option back, which is exactly what `children`-as-a-function is for. |
MentionInputTextarea
| Prop | Type | Description |
|---|---|---|
asChild | boolean | Render the consumer's own textarea-rendering component instead of a `Textarea` — e.g. `PromptInputTextarea`. The child must ultimately render a real `<textarea>`; everything this component needs is spread onto it. |