Message composer

Composes a compact growing draft with validated file previews, paste and drop, screenshot capture, and retained drafts on failure.

Continue the conversation

Enter to send. Shift+Enter for a new line.

Paste or drop files, or choose them from your device. Up to four files, 10 MB each. Sending is local to this example.

Use it for

  • Chat, comments, or a support reply.
  • Async submission that must preserve a draft when it fails.
  • Application-owned response generation with a Stop response action.
  • Use compact for a single-line draft with inline icon actions. The field grows with wrapping and newlines, then scrolls at maxRows, which defaults to six lines.
  • The compact field shrinks when text is removed or a successful send clears it, and remeasures when its available width changes.
  • Use files, defaultFiles, and onFilesChange to lift attachment selection alongside controlled text.
  • Picker, paste, and drop share accept, maxFiles, maxFileSize, and multiple validation. onAttachmentError receives rejected files and reasons.
  • Use tools for model selectors or application controls, and renderAttachments for custom preview composition.
  • useMessageComposer reads the current draft, files, previews, pending and generation state, and validated commands from a descendant. It requires a surrounding MessageComposer.
  • renderLayout receives MessageComposerParts and state to arrange input, submit, attach, screenshot, attachments, and tools. Render input and submit once; hidden file selection and feedback remain owned by the form.
  • textareaProps forwards native attributes, events, and a merged ref to the actual field. Prevent a keyboard event default to override its shortcut; required draft and submission behavior stay in the composer.
  • Enable globalDrop on one composer per page when file drops outside the field should attach there. allowScreenshot adds explicit browser screen selection when supported.

Not for

  • An arbitrary multi-field form or uploading files without a message.
  • Calling useMessageComposer outside its provider or invoking commands while rendering. Run commands from user actions or application effects.
  • Assuming browser file checks replace the application upload policy or that attachment selection uploads files automatically.

Three ways in. They share one source, so the pixels match whichever you pick.

1. Import the package

npm install @noorddev/vlak-react

// once, next to your app's root
import "@noorddev/vlak-react/css";

import { Button, MessageComposer, useMessageComposer, type ComposedMessage } from "@noorddev/vlak-react";

Precompiled React and one stylesheet. No compiler to configure. Per-component imports work too: @noorddev/vlak-react/components/message-composer.

2. Vendor the source

npx @noorddev/vlak-cli add message-composer

The StyleX leaf lands in components/vlak/ with its dependencies, for your own compiler to own. See StyleX.

3. Through shadcn

npx shadcn add https://vlak.dev/r/message-composer.json

The same registry item, installed by shadcn's CLI.

CSS only

<link rel="stylesheet" href="node_modules/@noorddev/vlak/css/vlak.css" />

No React. Link vlak.css and use the markup and classes below.

"use client";
import { Button, MessageComposer, useMessageComposer, type ComposedMessage } from "@noorddev/vlak-react";

function ClearDraft() {
  const draft = useMessageComposer();
  return <Button variant="subtle" size="sm" disabled={draft.disabled || !draft.value}
    onClick={() => { draft.setValue(""); draft.focus(); }}>Clear draft</Button>;
}

export function ProjectComposer({ onSend, generating = false, onStop }: {
  onSend: (message: ComposedMessage) => void | Promise<void>;
  generating?: boolean;
  onStop?: () => void;
}) {
  return <MessageComposer compact maxRows={6} sendOnEnter onSend={onSend}
    generating={generating} onStop={onStop} tools={<ClearDraft />}
    textareaProps={{ "aria-label": "Ask about the project", autoComplete: "off" }}
    allowAttachments accept="image/*,.pdf" maxFiles={4}
    maxFileSize={10 * 1024 * 1024} allowScreenshot />;
}

MessageComposer

A message draft with attachments, IME-safe shortcuts, and retained text after send failures.

PropTypeDefaultDescription
valuestring
defaultValuestring""
onValueChange(text: string) => void
onSendrequired(message: ComposedMessage) => void | Promise<void>
labelstring"Message"
placeholderstring"Write a message…"
disabledbooleanfalse
allowAttachmentsbooleanfalse
acceptstring
filesFile[]Controlled file selection, independent of the text draft.
defaultFilesFile[][]
onFilesChange(files: File[]) => void
multiplebooleantrue
maxFilesnumber
maxFileSizenumber
onAttachmentError(rejections: MessageAttachmentRejection[]) => void
globalDropbooleanfalseAlso accept file drops outside this composer. Enable on one composer per page.
allowScreenshotbooleanfalseShows a user-activated browser screen capture action when available.
toolsReactNodeApplication-owned model selectors, capability controls, or other tools.
renderAttachments(attachments: AttachmentData[], actions: MessageComposerAttachmentActions) => ReactNode
renderLayout(parts: MessageComposerParts, state: MessageComposerState) => ReactNodeRearranges the provided controls. Render input and submit once; feedback stays in the form.
textareaPropsMessageComposerTextareaProps{}Native textarea attributes and handlers. Prevent a key's default to override its shortcut.
maxLengthnumber
sendOnEnterbooleanfalseEnter submits, Shift+Enter inserts a line. Otherwise use Cmd/Ctrl+Enter.
generatingbooleanfalseApplication-owned response generation, separate from submission pending state.
onStop() => voidRequests that the application stop generation; does not itself cancel a network request.
compactbooleanfalseA single-line draft with an inline icon action; grows as the message wraps.
maxRowsnumber6Maximum visible draft lines in compact mode, clamped to 1–20.

Also accepts Omit<FormHTMLAttributes<HTMLFormElement>, "defaultValue" | "onSubmit">.

useMessageComposer (hook)

Reads the nearest composer's draft and validated commands from a custom child or tool.

KeysDoes
Cmd+Enter, Ctrl+EnterSubmits the draft when no send or response generation is in progress.
Enter, Shift+EnterWith sendOnEnter enabled, Enter submits and Shift+Enter inserts a line; IME composition never submits.
Tab, Enter, SpaceOperates attachment, send, and stop actions.
<form class="rs-message-composer" aria-label="Message"><label>Message<textarea class="rs-textarea" placeholder="Write a message…"></textarea></label><div class="rs-message-composer-actions"><button class="rs-btn-primary" type="submit">Send</button></div></form>
.rs-message-composer.rs-message-composer-compact.rs-message-composer-row.rs-message-composer-area.rs-message-composer-actions.rs-message-composer-action.rs-message-composer-icon-action.rs-message-composer-files.rs-message-composer-file.rs-message-composer-hint.rs-message-composer-input.rs-message-composer-sr-only.rs-message-composer-tools.rs-message-composer-drag