Skip to content
hightouchUI

Design system

47b1b91

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.

Initial
Active
Long label
Disabled
Picker
Compact · primary
Compact · warning

PromptInputToggle

Initial
Active
Long label
Disabled
Primary
Warning

PromptInputSubmitButton

Empty
Ready
Disabled
Loading, cancellable
Loading

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.
  • Badge and BadgeButton are 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 disabledMessage the 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 compact pill 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.

NameDefaultDescription
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.
isDisabledfalsebooleanToggles the disabled state.
variant

"picker" | "compact"A picker always shows its label plus a chevron.
isPlaceholderfalsebooleanRender the label muted, for a not-yet-selected prompt.
isActivefalsebooleanWhether 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.
ariaLabellabelstringLabel 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.

NameDefaultDescription
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.
isDisabledfalsebooleanToggles the disabled state.
variant

"picker" | "compact"A picker always shows its label plus a chevron.
isPlaceholderfalsebooleanRender the label muted, for a not-yet-selected prompt.
isActivefalsebooleanWhether 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.
isLoadingfalsebooleanAccepted 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.

NameDefaultDescription
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.
isDisabledfalsebooleanToggles 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.

NameDefaultDescription
hasContent

booleanWhether the prompt input has something to send. An empty prompt input renders the button idle and disabled.
isDisabledfalsebooleanBlocks sending even when the prompt input has content.
isLoadingfalsebooleanWhether 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.