Prompt input
Prompt input is the family of standalone pill-shaped pickers, toggles, and the submit button that make up the bottom action row of an LLM prompt input (also called a chat composer).
Example
PromptInput in this example is docs-only scaffolding, not a library export — the production prompt input container lives in the Hightouch app, so don't import it in product code. How it draws the chrome is documented in its source, docs/components/demos/prompt-input.js.
Usage
Picker
A PromptInputMenuButton with variant="picker" always shows its label and a chevron, so the current selection stays readable. It must be rendered inside a <Menu>, and the caller supplies the <MenuList> — the pill only styles the trigger. Use isPlaceholder for a prompt that hasn't been answered yet, and maxLabelWidth to change where a long label starts fading out (the label is masked with a fade rather than an ellipsis, so a truncated value never looks like part of the name).
A pill holds a selection, so fill the menu with a filter menu FilterMenuGroup of type="radio" rather than plain menu items — the open menu then checks the current value, and the pill's label and its menu can't disagree. Keep the <Menu> root: it closes the menu on select, which a picker wants and FilterMenu deliberately doesn't do.
Compact
A PromptInputMenuButton with variant="compact" is icon-only until it's active, then it expands to reveal its label and picks up a tint. Pass onClear to add a hover-revealed X that clears the choice without opening the menu, and tone="warning" when the active state is something the user should notice.
Pill button
PromptInputPillButton is the pill itself, without the menu wiring. Reach for it when the pill drives something other than a Menu — an onClick that opens a drawer, or a custom trigger.
It's also the shape to pass to a combobox's trigger={{ type: "component", ... }} when the picker needs search. The combobox hands the trigger everything it needs (onClick, aria-label, disabled and loading state), so spread those props onto the pill — isLoading is accepted and ignored, since the pill has no loading treatment.
Pass isLazy={false} with any non-input trigger: it renders the search field inside the popover, and a lazily mounted popover focuses that field before it has been positioned, which scrolls the page to the top on first open. popoverWidth keeps the option list from being sized to the pill.
Toggle
PromptInputToggle is a pill that turns a mode on and off. It renders as aria-pressed, so it's announced as a toggle button rather than a menu, and ariaLabel is required: the visible label only appears while the toggle is active, so the accessible name has to stay constant.
Disabled pills
A disabled PromptInputPillButton drops its tooltip and shows disabledMessage instead, or nothing at all. A pill with a reason stays focusable, so keyboard users reach it too.
Submit button
PromptInputSubmitButton owns every state of sending, plus the tooltip that explains it. It renders a primary circular icon button — reach for IconButton directly for any icon action that isn't submitting a prompt. The button sends only when the prompt input has content and neither isDisabled nor isLoading is set. When it is blocked, it shows disabledMessage if you pass one and stays silent if you don't. An empty prompt input (hasContent false) blocks it the same way isDisabled does. A blocked button with a reason stays focusable, so keyboard users reach it. While isLoading, passing onCancel turns the button into a Stop control, and omitting it shows a spinner instead.
Combinations
PromptInputMenuButton
One scenario throughout — each cell's menu holds the real option list with the shown value checked. For a picker, initial is the unanswered placeholder and active is a made selection; long label shows the overflow fade, and hovering any active compact cell reveals its clear ✕. PromptInputPillButton renders the same pixels as the menu button, so its states are covered here.
PromptInputToggle
PromptInputSubmitButton
Guidelines
When to use
- In the footer of a prompt input, for the row of controls that sits under the textarea.
- For compact inline pickers and toggles that belong to the prompt being written — a scope selector, a mode switch, an output format.
- For the send control of any prompt input, so submitting, blocking, and cancelling look and read the same across prompt surfaces.
When not to use
- For selection in a general form, use a select or a combobox — those carry labels, validation, and form-field sizing that a pill deliberately drops.
- For a page-level mode switch outside a prompt input, use a toggle button, a segmented control, or a switch.
- For a generic dropdown trigger, use a menu with a plain
MenuButton— the pill's shape only reads correctly inside a prompt input row. - For non-interactive status, use a badge.
BadgeandBadgeButtonare pill-shaped too, but they're labels: no expanding label, no menu, no toggle semantics, and no clear affordance. Reach for a prompt input pill only when the pill is a control in a prompt input.
Content
- Write a
disabledMessagethe user can act on. "You need write access to change the brand" tells them what to do about it. "Disabled" repeats what they can already see. - Leave it off when there is no reason to give, and for states that last a moment such as a permission check in flight or a session being created. The tooltip is gone before it is read.
- A
compactpill hides its label until it is active, so a disabled one shows only its icon. Name the control in the message as well as the reason. - Keep whatever control resolves the denial enabled. A message telling someone to pick a different model is no help if the model picker is disabled too.
Props
Every component inherits margin props.
PromptInputMenuButton
The menu-backed pill: the trigger inside a <Menu>, for choices that live in a dropdown. variant="picker" always shows the current selection with a chevron; variant="compact" is icon-only until active, then expands to reveal its label.
| Name | Default | Description |
|---|---|---|
label | — | stringPill text. A compact pill reveals it only while active. |
icon | — | ComponentType<SVGAttributes<SVGElement>>Leading glyph. A compact pill needs this or leadingAccessory; when
both are passed, the glyph renders first. |
leadingAccessory | — | ReactNodeCustom leading element, for content a plain glyph can't express. Size it
to promptInputPillIconSize — an inactive compact pill is a fixed 32px
disc, so a wider accessory overflows it. |
tooltip | — | stringHover hint. The pill owns its Tooltip. Silenced while disabled — pass
disabledMessage for the reason instead. |
disabledMessage | — | stringWhy the pill is disabled; shown as the tooltip in place of tooltip.
Without it a disabled pill says nothing on hover. |
maxLabelWidth | "130px" | stringWidth at which the label starts fading out. |
isDisabled | false | booleanToggles the disabled state. |
variant | — | "picker" | "compact"A picker always shows its label plus a chevron. |
isPlaceholder | false | booleanRender the label muted, for a not-yet-selected prompt. |
isActive | false | booleanWhether the pill is expanded and tinted. |
tone | "primary" | "primary" | "warning"Tint used while active. |
onClear | — | () => voidActive only: hover-revealed X that clears without opening a menu. |
clearAriaLabel | "Remove" | stringLabel of the clear X for screen readers. |
ariaLabel | label | stringLabel for screen readers. |
PromptInputPillButton
The plain action pill: the same shape without menu wiring, for an onClick that drives something other than a Menu — a drawer, a custom trigger.
| Name | Default | Description |
|---|---|---|
label | — | stringPill text. A compact pill reveals it only while active. |
icon | — | ComponentType<SVGAttributes<SVGElement>>Leading glyph. A compact pill needs this or leadingAccessory; when
both are passed, the glyph renders first. |
leadingAccessory | — | ReactNodeCustom leading element, for content a plain glyph can't express. Size it
to promptInputPillIconSize — an inactive compact pill is a fixed 32px
disc, so a wider accessory overflows it. |
tooltip | — | stringHover hint. The pill owns its Tooltip. Silenced while disabled — pass
disabledMessage for the reason instead. |
disabledMessage | — | stringWhy the pill is disabled; shown as the tooltip in place of tooltip.
Without it a disabled pill says nothing on hover. |
maxLabelWidth | "130px" | stringWidth at which the label starts fading out. |
isDisabled | false | booleanToggles the disabled state. |
variant | — | "picker" | "compact"A picker always shows its label plus a chevron. |
isPlaceholder | false | booleanRender the label muted, for a not-yet-selected prompt. |
isActive | false | booleanWhether the pill is expanded and tinted. |
tone | "primary" | "primary" | "warning"Tint used while active. |
onClear | — | () => voidActive only: hover-revealed X that clears without opening a menu. |
clearAriaLabel | "Remove" | stringLabel of the clear X for screen readers. |
isLoading | false | booleanAccepted so the pill can be a Combobox trigger.type: "component", and
ignored — the pill has no loading treatment. |
PromptInputToggle
The on/off pill: switches a mode and announces itself as a toggle (aria-pressed), revealing its label only while active.
| Name | Default | Description |
|---|---|---|
label | — | stringPill text, revealed while active. |
icon | — | ComponentType<SVGAttributes<SVGElement>>Leading glyph, always visible. |
isActive | — | booleanWhether the toggle is on. |
onToggle | — | () => voidCallback for when the toggle is clicked. |
ariaLabel | — | stringLabel for screen readers, needed because the label is hidden while inactive. |
tooltip | — | stringHover hint. Silenced while disabled — pass disabledMessage for the
reason instead. |
isDisabled | false | booleanToggles the disabled state. |
disabledMessage | — | stringWhy the toggle is disabled; shown as the tooltip in place of tooltip.
Without it a disabled toggle says nothing on hover. |
tone | "primary" | "primary" | "warning"Tint used while active. |
PromptInputSubmitButton
The send control: owns every state of sending — idle, active, blocked with a disabledMessage, and loading with an optional Stop — plus the tooltip that explains it. It builds on the primary circular IconButton; use it for sending or stopping a prompt input, and plain IconButton for every other icon action.
| Name | Default | Description |
|---|---|---|
hasContent | — | booleanWhether the prompt input has something to send. An empty prompt input renders the
button idle and disabled. |
isDisabled | false | booleanBlocks sending even when the prompt input has content. |
isLoading | false | booleanWhether a response is in flight. |
onSubmit | — | () => voidCallback for when the prompt is submitted. |
onCancel | — | () => voidWhile loading: with onCancel the button becomes a Stop control; without
it, a spinner. |
disabledMessage | — | stringWhy sending is blocked; shown as the tooltip instead of the send hint. |