Skip to content

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.

Do — mount one ComposerLensDefs per page that uses Composer. The filter IDs are global; multiple instances collide.
Don't — render Composer multiple times on the same screen. The glass surface is the canonical input — a screen with two Composers reads as a UX bug.

Anatomy

Three structural regions:

  1. 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.
  2. Textarea. Auto-growing, capped at 200px. Inherits glass-surface typography. Enter submits, Shift+Enter inserts a newline. Placeholder reads from the placeholder prop.
  3. Bottom row. Left slot for chips (typically ComposerChip instances — attach button, model picker, context picker). Right slot for the send button (icon by default, sendLabel override).

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

PropTypeDefaultDescription
valuestring
defaultValuestring""
onChange(value: string) => void
onSubmitrequired(value: string) => void
disabledbooleanfalse
placeholderstring"Ask Matter…"
chipsReactNode
sendLabelReactNodeSEND_ICON
lensRimbooleantrueOptional rim-band lensing. Defaults to true.
classNamestring
styleCSSProperties

Controlled and uncontrolled modes:

  • Uncontrolled. Pass defaultValue, read submissions via onSubmit(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

StateTriggerVisual
Default (empty)Initial mountPlaceholder visible, send button disabled
TypingUser inputTextarea auto-grows to 200px max; send button enabled
SubmittedonSubmit firesTextarea clears (uncontrolled) or consumer clears (controlled); focus stays in textarea
Disableddisabled propTextarea 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

colors1 tokensv2.3.0
rgba(229, 229, 229, 0.64)
brand
src ↗
No tokens match.
radii15 tokensv2.3.0
10
matter
src ↗
16
matter
src ↗
24
matter
src ↗
32
matter
src ↗
72
matter
src ↗
999
matter
src ↗
6
brand
src ↗
10
brand
src ↗
12
brand
src ↗
16
brand
src ↗
20
brand
src ↗
24
brand
src ↗
32
brand
src ↗
72
brand
src ↗
9999
brand
src ↗

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 carries aria-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 whatever sendLabel resolves 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 Canvas with ButtonBorder. Send button → ButtonFace/ButtonText.
  • WIG rules. Real <textarea> (semantic-element rule). Send icon-button carries aria-label="Send" per the icon-only-button rule. prefers-reduced-motion respected per the animation rule. touch-action: manipulation on the send button.

Do / Don't

Do — wrap the textarea in a <label> if the surrounding context doesn't make the input's purpose obvious from placeholder alone.
Don't — use Composer for a plain comment field. The glass surface is reserved for the agent-input pattern.
Do — pass lensRim={false} for the secondary Composer instance on a page that already has one. The first Composer carries the lens.
Don't — mount ComposerLensDefs more than once. The filter IDs are global; collisions break the displacement map.
Do — pass an array of ComposerChip instances to chips for attach / model / context affordances.
Don't — overload the chip slot with > 4 chips. The slot grows horizontally; beyond 4 it crowds the send button.

Recipes

This component appears in:

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/Composer

On this page