Composer
Hero example
When to use
Reach for Composer when the surface is a primary message-input pane talking to an agent or a recipient. Canonical mounts: the /assistant route, the /mail reply pane, the inline /documents/[id]/explain block, and the <RecordDrawer> "ask" tab. Composer is the only message-input primitive in the Matter design system — anywhere a user is composing text bound for a recipient or an agent, it's Composer.
Don't reach for Composer for inline search inputs (those are Timeline Search or a plain <input>), for plain comment fields outside the agent surface (use a <textarea> directly), or for AI-result panels that don't accept input. The Tahoe-glass refraction is computationally non-trivial (backdrop-filter + SVG feDisplacementMap) — only deploy it where the input is the surface's centre of attention.
Anatomy
Three structural regions:
- Glass surface. The card-level container with
backdrop-filter: url(#composer-lens)+ an optional rim-band that uses#composer-chroma. The lens IDs reference the<defs>chain rendered by ComposerLensDefs. - Textarea. Auto-growing, capped at 200px. Inherits glass-surface typography. Enter submits, Shift+Enter inserts a newline. Placeholder reads from the
placeholderprop. - Bottom row. Left slot for
chips(typically ComposerChip instances — attach button, model picker, context picker). Right slot for the send button (icon by default,sendLabeloverride).
The lens filter SVG must be mounted once on the page — Composer mounts it inline so it travels with the component. If you compose multiple Composers, render <ComposerLensDefs> once at the page root and pass lensRim={false} to all but the first.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | — |
| defaultValue | string | "" | — |
| onChange | (value: string) => void | — | — |
| onSubmitrequired | (value: string) => void | — | — |
| disabled | boolean | false | — |
| placeholder | string | "Ask Matter…" | — |
| chips | ReactNode | — | — |
| sendLabel | ReactNode | SEND_ICON | — |
| lensRim | boolean | true | Optional rim-band lensing. Defaults to true. |
| className | string | — | — |
| style | CSSProperties | — | — |
Controlled and uncontrolled modes:
- Uncontrolled. Pass
defaultValue, read submissions viaonSubmit(value). Simplest pattern. - Controlled. Pass
value+onChange(value)+onSubmit(value). Use when the consumer needs to manipulate the input (e.g. quoted-reply prefix on click).
States
| State | Trigger | Visual |
|---|---|---|
| Default (empty) | Initial mount | Placeholder visible, send button disabled |
| Typing | User input | Textarea auto-grows to 200px max; send button enabled |
| Submitted | onSubmit fires | Textarea clears (uncontrolled) or consumer clears (controlled); focus stays in textarea |
| Disabled | disabled prop | Textarea read-only, send button disabled, glass surface dimmed |
Density
The Composer is a hero-surface input and ignores the site density toggle — it always renders at comfortable density. The glass surface, refraction, and rim band are visual identity; compacting them would reduce them to a generic textarea.
Themes
In dark, the glass surface saturation increases to compensate for the lower ambient luminance. The lens displacement scale stays constant. In forced-colors, the glass collapses to Canvas with ButtonBorder; the lens filter has no effect because backdrop-filter is disabled.
Tokens consumed
The glass surface reads --surface-glass and --surface-glass-border. The send button reads --action-primary and --action-primary-hover. The lens-rim chromatic-aberration band has no token; the filter applies directly.
Accessibility
- Keyboard interactions. Textarea is native — full text-editing support. Enter submits; Shift+Enter newlines. Tab moves to chips; Shift+Tab returns to textarea. Send button is reachable via Tab after chips.
- ARIA roles and properties. Textarea has implicit role
textbox. Send button is<button type="button">. ComposerLensDefs SVG carriesaria-hidden="true". - Focus order. Textarea → chips (in slot order) → send button. Focus stays on textarea after submit.
- Screen-reader expectations. Textarea announces placeholder as the accessible name unless wrapped in a
<label>. Send button announces "Send" (or whateversendLabelresolves to). - Reduced motion. Backdrop refraction stays — it's static, not animated. Chromatic-aberration band loses its animated drift under
prefers-reduced-motion: reduce. - Forced colors. Glass collapses to
CanvaswithButtonBorder. Send button →ButtonFace/ButtonText. - WIG rules. Real
<textarea>(semantic-element rule). Send icon-button carriesaria-label="Send"per the icon-only-button rule.prefers-reduced-motionrespected per the animation rule.touch-action: manipulationon the send button.
Do / Don't
<label> if the surrounding context doesn't make the input's purpose obvious from placeholder alone.lensRim={false} for the secondary Composer instance on a page that already has one. The first Composer carries the lens.chips for attach / model / context affordances.Recipes
This component appears in:
- Docs corpus recipe — the document-detail "ask" pane.
- Board recipe — the board-meeting assistant input.
Code example
import { Composer, ComposerChip, ComposerLensDefs } from "@matter/components";
import { useState } from "react";
export function AssistantPane({ onAsk }: { onAsk: (q: string) => void }) {
const [model, setModel] = useState("claude-3-7-sonnet");
return (
<>
<ComposerLensDefs />
<Composer
onSubmit={onAsk}
placeholder="Ask Matter…"
chips={
<>
<ComposerChip label="Attach" icon="paperclip" />
<ComposerChip
label={`Model · ${model}`}
onClick={() => openModelPicker(setModel)}
/>
</>
}
/>
</>
);
}Source
packages/components/src/Composer/ComposerTimeline
Namespace of primitives for event-feed surfaces — Timeline, Audit log, Inbox. Three layers: atoms (KindDot, KindChip, ActorChip, SourceChip), filter controls (Search, FilterSelect, KindToggleRow), and shared constants (KIND_CFG, ACTOR_TONES, SOURCE_LABEL).
ComposerChip
Pill-shaped action chip rendered in the Composer's bottom-left slot. Used for attach buttons, model pickers, context selectors. Carries an optional leading icon and is a real <button>.