import { MessageForm } from "@elabs-ai/components-ai";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-aiPaste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.
Add the brand-ui MessageForm component (from @elabs-ai/components-ai) to this project. 1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`. 2. Look MessageForm up before writing code: `npx -y @elabs-ai/components-cli docs MessageForm` prints its real props, variants, what it pairs with and what to avoid. 3. If @elabs-ai/components-ai is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-ai` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-ai`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`. 4. Use it here: [the screen or file, and what it should show] 5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop. 6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.
Avoid
- Letting a malformed spec throw — model output is untrusted; an invalid spec renders MessageFormFallback.
- Surfacing validation errors while the spec is still streaming — half-arrived fields are dropped, not reported as errors.
- Letting the model choose the look — it authors the spec; the component owns layout, tokens and focus handling.
MessageForm — not a chat input. A model-emitted form rendered INSIDE a message. It sits next to Composer in the sidebar and reads like a sibling of it; it is not one. Spec-driven and zod-validated: the model is the author, it never chooses the look. A malformed spec degrades to MessageFormFallback rather than throwing, half-arrived fields are dropped while streaming, and a submitted form renders inert with its values still visible — a chat message is a historical record.
Works with
Examples
Streaming Partial
Streaming / partial — the spec is still arriving. Only the fields that have fully parsed render; the rest is a skeleton. Never a crash, never null holes.
Streaming Skeleton
Streaming with nothing parsed yet → a layout-shaped skeleton.
Provider Streaming
The compound composition — `MessageFormProvider` + the parts directly, instead of the `MessageForm` convenience wrapper (see the file-level doc comment's "compound structure" section). `isStreaming` lives on the provider itself since it is the one that owns the lifted form state; a custom layout that skips `MessageForm` still needs to pass it through here.
Empty
Empty (not streaming) → a calm fallback, never broken UI.
Fallback
Fallback — a malformed / non-object spec renders a typed fallback (no throw).
Validation
Validation — submit with an invalid email + missing required fields.
Controlled
Controlled — the parent owns `values`; here a live JSON preview reflects them.
Submitting
Submitting — the request is in flight (controls disabled, spinner on submit).
Submitted
Submitted — a terminal, inert historical record with values visible.
Dark Theme
Dark theme — verifies token-driven surfaces + focus rings in dark. The theme is scoped to this story via a `data-theme` decorator (NOT a `globals` override, which would stick and re-theme the whole Docs page).
API
MessageForm
| Prop | Type | Description |
|---|---|---|
specrequired | FormSpec | unknown | The serializable form specification produced by an LLM tool-call. |
values | FormValues | Controlled values (`{ [fieldName]: value }`). Omit for uncontrolled. |
onChange | (values: FormValues) => void | Called with the next full values object on any change. |
onSubmit | (state: FormSubmitState) => void | Called with `{ formName, values }` on a valid submit. |
submitLabel | string | Submit button label (overrides `spec.submitLabel`). |
disabled | boolean | Disable every control. |
submitted | boolean | Terminal submitted state: inert, values visible, submit replaced. |
submitting | boolean | In-flight submit: controls disabled, spinner on submit. |
isStreaming | boolean | The spec is still streaming (renders a skeleton when no fields yet). |
streaming | boolean | @deprecated Use `isStreaming`. |
MessageFormDescription
MessageFormFallback
| Prop | Type | Description |
|---|---|---|
message | string | Short human reason the form could not render. |
MessageFormField
| Prop | Type | Description |
|---|---|---|
namerequired | string | The field's `name` (its key in the spec + values). |
MessageFormFields
MessageFormProvider
| Prop | Type | Description |
|---|---|---|
specrequired | NormalizedFormSpec | A validated/normalized spec (a plain `FormSpec` also satisfies this). |
values | FormValues | Controlled values. When provided the component is controlled and `onChange` is the only way to update state. |
onChange | (values: FormValues) => void | Called with the next full values object on any field change. |
onSubmit | (state: FormSubmitState) => void | Called with `{ formName, values }` when a valid form is submitted. |
disabled | boolean | Disable every control (form is not interactable). |
submitted | boolean | Terminal submitted state: controls are inert, values visible, no submit. |
submitting | boolean | In-flight submit: controls disabled, the submit button shows a spinner. |
isStreaming | boolean | The spec is still streaming in (renders a skeleton when no fields yet). |
streaming | boolean | @deprecated Use `isStreaming`. |
childrenrequired | ReactNode |
MessageFormRoot
MessageFormSubmit
| Prop | Type | Description |
|---|---|---|
label | string | Submit button label (overrides `spec.submitLabel`). @default "Submit" |