Headless / Input components
Message composer
One generic form for every place a message is sent: a chat, a room, a comment thread, a plain form posted to a server action. It knows nothing about chats, uploads or dictation. It collects fields, holds the submit while any field is busy, and hands the message to the nearest sink or to its own onSubmit. Two contracts, field and sink, are all it relies on. It replaces Chat.Composer, Room.Composer and Upload.Composer, and the chat-attachments and chat-dictation bridges.
8 public parts · 2examples
@slices.sh/composerView as Markdown ↗Your design. Our behavior.
Explore the tree to see what each part controls. Gates reveal the relevant state; roots outline their scope.
Composer.Surface · Generic
Installation
Choose what your component needs. Run these commands in an initialized Next.js app.
Before you start
The steps below run in a Next.js app with shadcn set up (it has a components.json). In an existing Next.js app, such as one from create-next-app, set shadcn up first:
npx shadcn@latest init -t next -b base -p novaOr start fresh: the slices CLI creates the app and sets it up.
npx slices.sh installInstall the headless library
npmAdd the React parts to your app, then compose them with your own elements.
npm install @slices.sh/composer@^0.1.4Import from @slices.sh/composer.
These parts run locally. No service installation or provisioning is needed.
Start small
Parts bring the behavior. Use their default elements, or supply your own with render.
"use client"import { Composer } from "@slices.sh/composer"export function Example() { return ( <Composer.Root onSubmit={async (message) => { console.log(message) }} > <Composer.Input aria-label="Comment" /> <Composer.Submit>Post comment</Composer.Submit> <Composer.Pending>Sending…</Composer.Pending> </Composer.Root> )}Replace console.log with your application action. No service is required when you supply onSubmit.
API reference
Every public part used by this experience is listed below. Gates render children without adding an element. Element parts accept native element props and render; function children receive the state declared in their props. Scopes provide context rather than a visual element.
Composer.Rootscope · Generic · <form>
The form every message is sent from. It knows fields by name and one destination, and nothing about chats, rooms, uploads or dictation. → docs/v2/target-docs/DESIGN.md §5 The check for a destination is a component of its own, so a Composer under a switched-off sink renders nothing without having called a hook it will not use.
Props
1 documented props · Native <form> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/root.tsx
Composer.Inputinput · Generic · <textarea>
The text of the message, and a field named `text` (`name="…"` changes the key). A textarea rather than an input, because Shift+Enter has to mean something. Enter submits, Shift+Enter breaks the line, and an IME's Enter commits a candidate rather than sending half a word. No `rows`: the skin sizes it with `field-sizing: content`, so it grows with the message and stops at a maximum. Escape stops what the destination is doing and Backspace takes back the last chip it holds beside the draft (`stop` and `removeLast` of the sink), but only from an empty input, and never while composing, while a wrapper holds the field busy, or after a wrapper took the key. It is busy while a wrapper has set `aria-busy="true"` on it, which is how `Dictation.Field` holds the submit while listening without either part knowing the other. It takes `value` and `onChange` from such a wrapper when it is given them, and holds its own text when not.
Props
2 documented props · Native <textarea> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/input.tsx
Composer.Submitaction · Generic · <button>
The form's only submit button: everything else in a composer is `type="button"`. → docs/rules/one-submit-per-form.md It stays mounted and enabled while the submit is held, so focus never drops and the layout never moves; `aria-disabled` and `data-held` say it instead, and a press does nothing. Stopping a streaming reply is not a submit: that is the sink's own part.
Required scope
Composer.Submit must be inside <Composer.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { held: boolean; pending: boolean }) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/submit.tsx
Composer.Surfacecontent · Generic · <div>
A wider place than the form that still hands files to it: a drop or a paste anywhere on a chat panel goes to the field named `attachments` of the Composer inside it. The field is found when the event arrives, through the forms below, so nothing here knows about uploads. With no such field (the service is off, the sink takes no files, there is no Composer) every event goes by untouched. Events are forwarded even when an element inside already took them: the field decides what an event that its own drop area handled means, since it counts enters and leaves across both.
Props
3 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
fieldThe field that takes what lands here. | string | "attachments" |
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/surface.tsx
Composer.Errorgate · Generic
Renders after the sink rejected, or `accept` refused a field. The draft is still there.
Required scope
Composer.Error must be inside <Composer.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { error: globalThis.Error; code: string | null }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/gates.tsx
Composer.Pendinggate · Generic
Renders while `sink.submit` is in flight: a slow room, a server action.
Required scope
Composer.Pending must be inside <Composer.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/gates.tsx
Composer.Heldgate · Generic
Renders while the submit is held: a field is busy, or the sink says `hold` and is.
Required scope
Composer.Held must be inside <Composer.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { reason: "field" | "sink"; fields: string[] }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/gates.tsx
SinkProvidercontent · Generic
Offers a sink to every Composer below it. A root that can receive messages renders this around its children. `null` is a sink switched off: an optional root whose service is missing says so, and a Composer below renders nothing instead of throwing `slices/no-sink`.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
valueRequired | Sink | null | — |
children | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/composer/src/sink.tsx
When something is missing
- No service required
- These parts run locally. Their parent contexts and required props still apply.
- Missing capability or parent scope
- Follow each part’s context requirements and the example composition. Capability-aware controls hide when the provider cannot perform their action.