Handoff: ChatInput
What and why
The field for composing a prompt on chat and agent screens. PDS has Textarea, but its border sits on the <textarea> itself, so there is nowhere to put a toolbar (add menu, model selector, voice, Send) inside the box. ChatInput moves the border, background, focus ring and resize handle to a wrapper (the "field") that holds the textarea and the toolbar. It is the largest of the chat components: four slots (aboveField, fieldHeader, actionsStart, actionsEnd), two variants (default, compact) and opt-in file drag and drop. It holds no buttons or text of its own; the screen fills the slots. Figma: Chat Input page (the designer's hand-built components, not a capture). Context: context.md.
Exploration and decisions
- A new component, not a
Textareaprop: the wrapper has to own the border so the toolbar sits inside it. Label, helper message, focus ring,sizeandisResizablemirrorTextarea, and the markup reuses the package'spds-input,pds-input-labelandpds-input-messageclasses (InputLabelandInputMessageare not exported). - Slots, not built-in buttons. The story composes
IconButton,MenuButton(add menu), the bareSelect(model) and a primaryIconButton(Send). The model selector was a separateModelSelectorproposal; it was replaced bySelectwithvariant='bare'on 2026-10-19. aboveField(a grey context bar on the top edge, outside the border) andfieldHeader(attachments, inside the border) are named by position. Both work in both variants. Attachments go infieldHeader; the idea of putting them inaboveFieldwas dropped (2026-10-21).compactis a single-line bar with the controls on the same row, standard input radius (not a pill), not resizable. The DOM follows the visual order so keyboard order matches. It keeps the rounded-square Send button, not the reference's circle.- Label hidden by default (
showLabelisfalse), still required and rendered for screen readers. - Drag and drop is opt-in through
onFilesDrop; the component only reports the droppedFile[]. Only file drags are intercepted, so dragging text into the textarea still works. The drag-over look is a dashed focus border on the info-blue fill. - Left out of the first version (present in
Textarea): required marker, label tooltip, character counter, read-only,inputWidth, translation strings, validation states. Chat prompts have nothing to validate; send failures belong in the conversation. - No separate attach button in the samples; the add menu has "Add photos & files".
Full list and reasons: decisions.md.
API
ChatInput (a div; extends div props except defaultValue, onChange and placeholder; the ref goes to the <textarea>):
| Prop | Type | Default | Purpose |
|---|---|---|---|
aboveField |
ReactNode |
none | Bar on the top edge, outside the border (for example the branch the prompt applies to). |
actionsEnd |
ReactNode |
none | Toolbar controls at the end, inside the field (model selector, voice, Send as a primary IconButton). |
actionsStart |
ReactNode |
none | Toolbar controls at the start, inside the field (add menu). |
className |
string |
none | Additional class names. |
defaultValue |
string |
none | Initial value (uncontrolled); not with value. |
disabled |
boolean |
false |
Disables the textarea and turns off drop. |
dropLabel |
string |
none | Text over the field while files are dragged onto it; without it only the border and fill change. |
fieldHeader |
ReactNode |
none | Inside the border above the text (attached files). In compact it sits above the row. |
id |
string |
none (req.) | ID of the textarea; also the name, and the base of the label and message ids. |
isResizable |
boolean |
true |
Drag the bottom corner to resize. Ignored in compact. |
label |
string |
none (req.) | Label of the field. |
message |
ReactNode |
none | Helper message below the field; linked with aria-describedby. |
onChange |
(event: ChangeEvent<HTMLTextAreaElement>) => void |
none | Change handler (controlled use). |
onFilesDrop |
(files: File[]) => void |
none | Turns on file drop; receives the dropped files. |
placeholder |
string |
none | Text shown when empty. |
rows |
number |
3 |
Visible rows above the toolbar (always 1 in compact). |
showLabel |
boolean |
false |
Show the label; otherwise it is visually hidden. |
size |
's' | 'm' |
's' |
Padding (--chat-input-padding). |
textareaProps |
ComponentPropsWithoutRef<'textarea'> |
none | Extra props for the <textarea>; spread last, so they override. |
value |
string |
none | Value (controlled); not with defaultValue. |
variant |
'default' | 'compact' |
'default' |
default: tall field, toolbar underneath. compact: one row. |
Variants and states: default and compact; sizes s and m; disabled; resizable or not; drag-over (pds-is-drag-over, with and without dropLabel); each slot filled; failed attachments with a helper message. Stories: Default, Compact, With Above Field, With Field Header, With Attachment Errors, With All Properties, Slots (Usage page only).
Tokens used
--pds-border-radius-input--pds-border-width-default--pds-border-width-outline--pds-color-border-default-secondary--pds-color-border-input--pds-color-foreground-default--pds-color-foreground-default-secondary--pds-color-input-placeholder--pds-color-interactive-focus--pds-color-status-critical-foreground--pds-color-status-info-background--pds-color-status-info-foreground--pds-color-surface-default--pds-color-surface-default-secondary--pds-spacing-2xl--pds-spacing-2xs--pds-spacing-4xs--pds-spacing-s--pds-spacing-xs--pds-typography-ff-default--pds-typography-lh-l--pds-typography-size-s--pds-typography-size-xs
npm run check:tokens -- ChatInput: 0 problems. --pds-color-foreground-default-secondary, --pds-spacing-2xl, --pds-spacing-4xs, --pds-typography-size-xs and the critical foreground color are used only in chat-input-story.css (story scaffolding), not in the component.
Impact
From impact.md: no existing PDS component is changed and no --pds-* token is redefined; everything is scoped to .pds-chat-input*. The only shared class is .pds-is-resizable, used as .pds-chat-input__field.pds-is-resizable, so existing uses are unaffected. No other proposal imports it by code (the Chat with an agent recipe uses it in its story; port ChatInput before it).
Reused vs net-new
- Reused: the package classes
pds-input,pds-input-label,pds-input-message,pds-is-disabled,pds-is-resizable,visually-hidden. Composed in stories (slots): localIconButton,MenuButton,Select(bare variant),ChatAttachment, andIcon. - Net-new:
.pds-chat-inputwith--compact,--m,__above-field,__actions,__actions-end,__actions-start,__drop-label,__field,__field-header,__row,__textarea; the state classpds-is-drag-over; all props; one custom variable,--chat-input-padding. No raw values kept. Details incustom.md.
Accessibility
- A real
<label>tied to the textarea byhtmlFor; hidden visually by default withvisually-hidden, still read by screen readers.messageis linked witharia-describedby. - The textarea is native, so keyboard and typing behave as usual. Focus: the ring is
:focus-withinon the field (--pds-border-width-outline,--pds-color-interactive-focus), so it also shows when a toolbar control has focus. Toolbar controls keep their own focus styles and keyboard behavior (IconButton, MenuButton, Select). - Compact: the DOM order is start controls, text, end controls, matching the visual order.
- Drag and drop is a pointer shortcut; the
dropLabeloverlay isaria-hidden. Keyboard users add files through the add menu (the screen must provide that path). - Failed attachments: the screen writes one
role='alert'helper message (shown in the story). - The a11y panel has not been run, and light and dark were not checked by eye in a session (the drag-over colors were noted as not checked in dark mode).
Decisions and open questions
For the engineer or designer to settle:
- Enter and Shift+Enter: left to the screen (the story's helper text only says "Enter to send"). Should send-on-Enter be built in, with an IME-composition guard?
- Agentic border: should the field get the AI gradient border (for example while the agent works), like Button? Not added.
- Compact growth: it is one row now; should it grow to a maximum height as a long prompt is typed? The reference's voice-mode waveform when empty is not included.
- Character counter or maximum length: not built. Does chat need one?
- Menu features
MenuButtondoes not support but the references show: trailing status or shortcut hints, selected toggles (on/off tools; the most likely need), submenus and count badges. Each would extendMenuButton's item type. - The model selector shows a chevron (the Select's trigger icon); the design shows text only. Confirm.
- Drag-over: which
dropLabelwording, and should the overlay hide the textarea as in the references? - Attachments: should
fieldHeaderget a max height with scroll when many files are attached? Should the bar show incompact? AnerrorTypeto drive retry versus remove-only is also open. textareaPropsis spread after the built-in props, so it can overrideid,rowsoronChange; confirm that is acceptable.- The voice icon
microphoneLinesis staged in the Icon proposal; the port adds it to the icon registry.
Port notes
Would live in src/components/ in an inputs or chat category folder (generator npm run generate:component). Not yet matching v2: the local mergeClasses stand-in (use @libs/components/utils), and the markup reuses pds-input* classes instead of the unexported InputLabel and InputMessage (use those if the port can import them). The textarea declares font family, size and line height from primitives (no pds-copy-* class matches the line height). The story composes local proposals, so the port needs them first.
Dependencies on other proposals:
- Select (
Component Proposals/Select, bare variant): ready for dev; ChatInput composes it, so port Select first. - ChatAttachment (
AI Chat/Chat Attachment): ready for dev; used in stories only (thefieldHeadersamples), so it does not block the component. - Marked ready: IconButton (its
primaryvariant andisWorkingare the Send button; they use Spinner and thestopicon in the Icon wrapper), MenuButton, Icon and Button. - Port order: see
docs/PORT-ORDER.md(node scripts/port-order.mjs --needs=ChatInputprints the graph); ChatInput goes after Select.
Preview
npm run storybook, story Component Proposals / AI Chat / Chat Input. Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=213-43, page ↳ Chat Input (the designer's hand-built components: the ChatInput component set with Default/Example content and Default/Compact size, and the prototype elements; not a capture).