---
title: Composer
description: Rich-text chat input with chips, slash/mention commands, attachments, and an ask-user flow.
source: composer
---
## Usage guidelines
- **Chat input** — a hand-rolled contenteditable over a flat segment model: native typing and IME, inline chips, attachments.
- **Prefix commands** — type `/`, `@`, or other prefixes to open command lists.
- **Panel** — hosts command results, live steps, or an ask-user prompt above the field.
- **Headless + styled** — behavior lives in `@intentface/chat`; the styled wrapper below is yours to copy and edit.
- **Get started** — see [Installation](/docs/installation) to add the package and copy the component.
## Anatomy
The bare nesting — every part is optional except `Composer` and `Container`:
```tsx
{(composer) =>
composer.commands.active && (
{(item) => (
{item.label}
)}
)
}
```
`Composer.Panel` takes plain children or a callback receiving the composer
state, and shows only while its resolved content is non-empty. Gate each part on
the state it belongs to (`commands.active`, `askUser.active`, …) and the panel
opens and closes to match — priority is just the order of your branches.
## Examples
### Mention command list
Type `@` to open the command list — the panel routes to it automatically while
a prefix is active. `commands` maps each prefix to its config and items.
### Floating command popover
`Composer.Popover` is the floating alternative to `Composer.Panel`. It takes the
same children — plain nodes or a state callback — but portals them above the
field, anchored to the active trigger token, so the list overlays instead of
growing the composer and needs no reserved height. It's collision-aware — near a
viewport edge it flips, shifts, and caps its height to stay on screen. Mount one
or the other; the content is identical.
### Ask-user flow
Setting the `questions` prop — typically from an assistant's clarifying
question — arms the ask-user flow and flips `askUser.active`; render
`` inside a `Panel` (or `Popover`) gated on that flag. The
flow steps through each question (single- or multi-select), and answering or
skipping the last one fires `onSubmit` with `{ kind: "answers" }`. Passing a
fresh `questions` array re-arms it from the first step.
### Attachments
`Composer.Attachments` renders the file strip and drop zone above the input;
`Composer.AttachmentTrigger` opens the file dialog. Files can also be dropped
onto the composer.
### Controlled value
`Composer.Textarea` accepts a controlled plain-text `value` with
`onValueChange`. Here the parent's buttons drive the field and typing reports
back.
### External store
`Composer.createStore()` returns a handle you own. Pass it via `store` and
drive the composer from anywhere — a toolbar, a shortcut — through
`store.controller`, with no context or ref threading.
## Multiple instances
Every `` creates its own isolated store, so several composers can
live on one page with no wiring. To reach a composer from outside its tree —
toolbars, keyboard shortcuts, status bars — create an explicit handle with
`Composer.createStore()`, pass it via the `store` prop, and use
`store.controller` (imperative) or `useComposerStore(store, selector)`
(reactive). See [Composer state](/docs/headless/state) for the full model.
## Performance
Typing costs nothing outside the composer: editor state lives in the store and
parts subscribe by slice with `useComposer(selector)`, so a keystroke
re-renders only the parts that read the changed slice — never your app tree.
The integration risk runs the other way: a streaming chat re-rendering the
composer on every chunk. The editor is the most expensive thing to re-render
per token, so isolate it behind a thin bridge — subscribe to your messages
state in a small component, derive the panel props there, and hand them to a
memoized inner composer:
```tsx
const ChatInput = () => {
const { messages, status } = useChatMessages();
const panelState = useAskUserPanelState(messages, status);
return ;
};
const ChatInputInner = memo(({ panelState, status }: ChatInputInnerProps) => (
));
```
The derivation (`useAskUserPanelState` here) is app code — what to surface in the
panel (an ask-user prompt, a status line, …) is your product's policy, so you own
it. Return a referentially stable value while nothing transitioned, so the inner
composer bails on every chunk except real panel changes. See
[Streaming performance](/docs/headless/performance) for the full render model.
## Keyboard
`Composer.Root` is a `