Skip to content
txbrendan/ui

Composer

beta

A message to people, written and sent in place, with who it reaches above it and Undo for 8 seconds after Send.

Preview

    Goes to , Juniors' parents get it instead

    Noor Haddad won't get it: no parent on file.

    Goes to Josh Tait

    Install

    1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

      npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
    2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

      npx shadcn@latest add @txbrendan/theme
    3. 3. 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

    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" />