Skip to content

Composer

The chat composer is a rounded, toolbar-equipped message input inspired by modern assistant UIs (Claude, Perplexity). It pairs an auto-growing textarea with a bottom toolbar split into left-aligned tools and right-aligned actions. It is themed entirely through component-scoped custom properties layered on the global --ds-* tokens, so it follows data-theme automatically and also offers an explicit cmp-chat-composer--dark modifier.

Icon buttons use inline SVGs sized by the cmp-chat-composer__icon class (a 1.25rem icon box), not text glyphs. It is commonly composed with the model selector, menu, and attachment components shown inside its header and toolbar.

The examples below are the canonical markup contract for this component: the same cmp-* classes and DOM structure used in production (for example the ID Assistant). Copy the HTML from the code block under each preview, pair it with the design system stylesheet (base.css from the CDN or a local build), and add js-chat-composer on the <form> if you want auto-grow and Enter-to-send without your own framework.

Nested pieces (menu, model selector, attachment) use the same pattern on their own pages. Popover markup is omitted when closed; open-state examples live on those pages.

Add the js-chat-composer hook to the <form> for progressive enhancement: the textarea auto-grows with its content (up to its CSS max-height), and pressing Enter submits the surrounding form while Shift+Enter inserts a newline. The examples below are live, so the textareas grow as you type.

The optional __header holds staged attachments, the __input is the message field, and the __toolbar carries __tools (left) and __actions (right).

syllabus.pdf
<form class="js-chat-composer cmp-chat-composer" onsubmit="return false;">
<div class="cmp-chat-composer__header">
<span class="cmp-chat-attachment">
<span class="cmp-chat-attachment__name">syllabus.pdf</span>
<button
type="button"
class="cmp-chat-attachment__remove"
aria-label="Remove syllabus.pdf"
title="Remove"
>
<p>×</p>
</button>
</span>
</div>
<textarea
class="cmp-chat-composer__input"
rows="1"
placeholder="Type a message... (Enter to send, Shift+Enter for new line)"
aria-label="Message"
></textarea>
<div class="cmp-chat-composer__toolbar">
<div class="cmp-chat-composer__tools">
<div class="cmp-chat-menu">
<button
type="button"
class="cmp-chat-composer__action cmp-chat-composer__action--soft"
aria-haspopup="menu"
aria-expanded="false"
aria-label="More options"
title="More options"
>
<svg
class="cmp-chat-composer__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
</div>
<div class="cmp-chat-composer__actions">
<div class="cmp-chat-model-select">
<button
type="button"
class="cmp-chat-composer__control cmp-chat-composer__control--ghost cmp-chat-model-select__trigger"
aria-haspopup="listbox"
aria-expanded="false"
aria-label="Model: GPT-5.5"
title="Model"
>
<span class="cmp-chat-model-select__trigger-label">GPT-5.5</span>
<svg
class="cmp-chat-model-select__chevron"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<button
type="submit"
class="cmp-chat-composer__action cmp-chat-composer__action--send"
aria-label="Send"
title="Send"
>
<svg
class="cmp-chat-composer__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
</div>
</div>
</form>

Set data-theme="dark" on an ancestor, or add the cmp-chat-composer--dark modifier for explicit, attribute-free dark usage. The modifier overrides only the component tokens; every rule is reused as-is.

<form
class="js-chat-composer cmp-chat-composer cmp-chat-composer--dark"
onsubmit="return false;"
>
<textarea
class="cmp-chat-composer__input"
rows="1"
placeholder="Type a message... (Enter to send, Shift+Enter for new line)"
aria-label="Message"
></textarea>
<div class="cmp-chat-composer__toolbar">
<div class="cmp-chat-composer__tools">
<div class="cmp-chat-menu">
<button
type="button"
class="cmp-chat-composer__action cmp-chat-composer__action--soft"
aria-haspopup="menu"
aria-expanded="false"
aria-label="More options"
title="More options"
>
<svg
class="cmp-chat-composer__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
</div>
<div class="cmp-chat-composer__actions">
<button
type="submit"
class="cmp-chat-composer__action cmp-chat-composer__action--send"
aria-label="Send"
title="Send"
>
<svg
class="cmp-chat-composer__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
</div>
</div>
</form>
ClassElementPurpose
cmp-chat-composerformRounded, flex-column composer root.
cmp-chat-composer--darkformExplicit dark variant (overrides tokens only).
cmp-chat-composer__headerdivOptional area above the input for attachment chips.
cmp-chat-composer__inputtextareaAuto-growing message field.
cmp-chat-composer__toolbardivBottom control bar (space-between).
cmp-chat-composer__toolsdivLeft-aligned tool group.
cmp-chat-composer__actionsdivRight-aligned action group.
cmp-chat-composer__actionbuttonCircular icon button (attach, send, etc.).
cmp-chat-composer__action--sendbuttonPrimary send affordance in the accent color.
cmp-chat-composer__action--softbuttonSquared menu-style affordance that darkens on hover.
cmp-chat-composer__iconsvg1.25rem inline icon box; apply directly to the <svg>.
cmp-chat-composer__controlbuttonPill control for grouped affordances.
cmp-chat-composer__control--ghostbuttonBorderless text control (e.g. the model selector trigger).
  • Give the textarea an accessible name with aria-label (or an associated label).
  • Icon-only buttons (__action) require an aria-label; mark the <svg> aria-hidden="true".
  • Keep a visible focus indicator; the component supplies a focus ring on :focus-visible.

The composer reads global semantic tokens with UGA literal fallbacks: --ds-surface, --ds-fg, --ds-border, --ds-fg-muted, --ds-surface-muted, --ds-hover-bg, --ds-accent, and --ds-accent-fg. Per-instance overrides are exposed as --cmp-chat-composer-* custom properties (for example --cmp-chat-composer-radius and --cmp-chat-composer-accent).