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.
Portable HTML
Section titled “Portable HTML”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.
Behavior
Section titled “Behavior”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.
Structure
Section titled “Structure”The optional __header holds staged attachments, the __input is the message
field, and the __toolbar carries __tools (left) and __actions (right).
<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>Dark variant
Section titled “Dark variant”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>Classes
Section titled “Classes”| Class | Element | Purpose |
|---|---|---|
cmp-chat-composer | form | Rounded, flex-column composer root. |
cmp-chat-composer--dark | form | Explicit dark variant (overrides tokens only). |
cmp-chat-composer__header | div | Optional area above the input for attachment chips. |
cmp-chat-composer__input | textarea | Auto-growing message field. |
cmp-chat-composer__toolbar | div | Bottom control bar (space-between). |
cmp-chat-composer__tools | div | Left-aligned tool group. |
cmp-chat-composer__actions | div | Right-aligned action group. |
cmp-chat-composer__action | button | Circular icon button (attach, send, etc.). |
cmp-chat-composer__action--send | button | Primary send affordance in the accent color. |
cmp-chat-composer__action--soft | button | Squared menu-style affordance that darkens on hover. |
cmp-chat-composer__icon | svg | 1.25rem inline icon box; apply directly to the <svg>. |
cmp-chat-composer__control | button | Pill control for grouped affordances. |
cmp-chat-composer__control--ghost | button | Borderless text control (e.g. the model selector trigger). |
Accessibility Guidelines
Section titled “Accessibility Guidelines”- Give the textarea an accessible name with
aria-label(or an associated label). - Icon-only buttons (
__action) require anaria-label; mark the<svg>aria-hidden="true". - Keep a visible focus indicator; the component supplies a focus ring on
:focus-visible.
Theming
Section titled “Theming”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).