Composer
betaA message to people, written and sent in place, with who it reaches above it and Undo for 8 seconds after Send.
Preview
Goes to 7 people, Juniors' parents get it instead
Noor Haddad won't get it: no parent on file.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/composer
Use it for
- Writing to a team, a group, or one person, under their thread.
- Pass everyone in to, as for Audience. Juniors go by juniorMessages.
- Show the message in its thread from onSend. Take it out in onUndo. Deliver it in onCommit, when the 8 seconds end.
- Pass later with times to offer Send later, like Mon 08:00.
Avoid it for
- An announcement in a sheet. Put Audience above a Textarea there, and make Send the sheet's main action.
- A one-line reply to a comment. Use Inline Reply.
- Long, formatted writing. Use Editor.
Props
- to
Required. Who the message is for. See Audience.
Recipient[]
- toLabel
What the group is called, like Art class. It titles the count's peek and fills the placeholder.
string
- juniorMessages
Where messages to juniors go: to their parents instead, or to the junior with their parents copied.
"guardians" | "copy" · default "guardians"
- placeholder
The field's placeholder.
string · default "Write to {toLabel}"
- defaultValue
Words to start with, like a draft.
string
- later
Times to offer under Send later. Without it, there is only Send.
{ id: string; label: string; at: string | Date }[]
- sendLabel
Send's words.
string · default "Send"
- onSend
Pressed: show the message where it lands. at is null for now, or the chosen later time.
(message: { text: string; at: string | Date | null; reach: AudienceReach }) => void
- onUndo
Taken back: take it out of the thread. The words are back in the field. The same object onSend got.
(message) => void
- onCommit
Final: deliver it now. Return a promise, and a failure turns the button into Retry.
(message) => void | Promise<unknown>
- onAddGuardian
The fix for a junior with no parent on file. See Audience.
(junior: { person: string; photo?: string }) => void
- size
Text, field, and button size.
"sm" | "md" | "lg" · default "md"
- disabled
Turns the field and the buttons off.
boolean · default false
- className
Extra classes on the root. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| to | Recipient[] | - | Required. Who the message is for. See Audience. |
| toLabel | string | - | What the group is called, like Art class. It titles the count's peek and fills the placeholder. |
| juniorMessages | "guardians" | "copy" | "guardians" | Where messages to juniors go: to their parents instead, or to the junior with their parents copied. |
| placeholder | string | "Write to {toLabel}" | The field's placeholder. |
| defaultValue | string | - | Words to start with, like a draft. |
| later | { id: string; label: string; at: string | Date }[] | - | Times to offer under Send later. Without it, there is only Send. |
| sendLabel | string | "Send" | Send's words. |
| onSend | (message: { text: string; at: string | Date | null; reach: AudienceReach }) => void | - | Pressed: show the message where it lands. at is null for now, or the chosen later time. |
| onUndo | (message) => void | - | Taken back: take it out of the thread. The words are back in the field. The same object onSend got. |
| onCommit | (message) => void | Promise<unknown> | - | Final: deliver it now. Return a promise, and a failure turns the button into Retry. |
| onAddGuardian | (junior: { person: string; photo?: string }) => void | - | The fix for a junior with no parent on file. See Audience. |
| size | "sm" | "md" | "lg" | "md" | Text, field, and button size. |
| disabled | boolean | false | Turns the field and the buttons off. |
| className | string | - | Extra classes on the root. Later classes win. |
Accessibility
- The field is named Message to {toLabel}, and Audience's line describes it, so people hear who it goes to.
- ⌘Enter or Ctrl+Enter sends from the field. Send's tooltip teaches it, and Send has aria-keyshortcuts.
- Send says what happened in its name: Sent to 3, then Sent to 3. Press to undo. The page's live region says it once.
- Undo arms a moment after a key press, or when the pointer leaves after a click, so a double press never undoes.
- In the empty field, ⌘Z takes back the message that just went and puts the words back. Anywhere else, ⌘Z does what the page's ⌘Z does.
- Send later is a menu button. Once a time is chosen, it says when, and a press on it undoes.
Examples
To a group, with Send later
<Composer
to={group}
toLabel="Art class"
later={[{ id: "mon", label: "Mon 08:00", at: "2026-10-26T08:00" }]}
onSend={(message) => setThread((all) => [...all, message])}
onUndo={(message) => setThread((all) => all.filter((m) => m !== message))}
onCommit={(message) => deliver(message)}
/>To one person
<Composer to={[{ person: "Josh Tait" }]} size="sm" />