Components

Instant Message Composer

Composer for SMS and WhatsApp with channel switching, recipient chips, attachments, and SMS character counting.

Client Only

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-instant-message-composer
UI Primitives(4 components)
npx shadcn@latest add badge button separator tooltip

Usage

import {
  InstantMessageComposer,
  type InstantMessageChannel
} from "@docyrus/ui/components/instant-message-composer";

<InstantMessageComposer
  variant="default"
  size="default"
  channel="sms"
  onChannelChange={(channel) => {}}
  recipients={['+15551234567']}
  onRecipientsChange={(recipients) => {}}
  body="Hello!"
  onBodyChange={(body) => {}}
  onSend={() => {}}
  onAttach={() => {}}
  onDiscard={() => {}}
/>

The composer is fully controlled. The send button is enabled once at least one valid phone number is entered and the body (or an attachment, on WhatsApp) is non-empty. Phone numbers are validated against a permissive E.164-style regex (6–18 digits, optional + prefix, common separators allowed); invalid chips are shown with a destructive style so users can correct them before sending.

Channels

The composer renders a small SMS / WhatsApp segmented switcher above the recipients row. Pass availableChannels to limit the choices, or omit onChannelChange to lock the composer to a single channel.

// Lock to WhatsApp only
<InstantMessageComposer
  channel="whatsapp"
  availableChannels={['whatsapp']}
  recipients={recipients}
  onRecipientsChange={setRecipients}
  body={body}
  onBodyChange={setBody} />

SMS character counting

When channel="sms", the footer shows a live character counter and segment estimate (160 chars · 1 SMS). Concatenated SMS uses 153 characters per segment to account for UDH overhead. The body is also clamped to maxLength (default 1600 for SMS, unlimited for WhatsApp). Pass an explicit number to override, or null to remove the limit.

WhatsApp formatting

WhatsApp messages support inline formatting using their native syntax — *bold*, _italic_, ~strike~, and ```mono```. The composer hints this under the body when the WhatsApp channel is active. Attachments and the attach button are only shown for the WhatsApp channel.

Variants

VariantDescription
defaultDefault style with input border and shadow
outlineOutline border, no shadow
minimalNo border or shadow — for embedding in other surfaces (dialogs, sheets)

Sizes

SizeDescription
smSmall — compact text and shorter body min-height
defaultDefault — comfortable text and 150px min body
lgLarge — bigger text and taller body min-height

API Reference

PropTypeDefaultDescription
variant"default" | "outline" | "minimal""default"Visual style
size"sm" | "default" | "lg""default"Sizing scale
channelInstantMessageChannel—Active channel: 'sms' or 'whatsapp'. Required.
onChannelChange(channel: InstantMessageChannel) => void—Called when the user switches channels. Omit to lock the channel.
availableChannelsReadonlyArray<InstantMessageChannel>['sms', 'whatsapp']Channels offered in the segmented switcher
recipientsstring[]—Recipient phone numbers (chip list). Required.
onRecipientsChange(recipients: string[]) => void—Called when chips are added/removed/pasted
bodystring—Plain-text message body. Required.
onBodyChange(body: string) => void—Called when the body changes
onSend() => void—Fired when the user clicks Send
onAttach() => void—Fired when the user clicks the paperclip (WhatsApp only)
onDiscard() => void—Fired when the user clicks Cancel
sendingbooleanfalseShows a spinner on the send button and disables interactions
disabledbooleanfalseDisables all inputs and actions
attachmentsInstantMessageAttachment[]—List of attached files (WhatsApp only)
onRemoveAttachment(index: number) => void—Called when an attachment chip is removed
maxLengthnumber | null1600 for SMS, no limit for WhatsAppHard cap on body length. Pass null to remove the limit.
helperReactNode—Optional helper rendered between the recipients row and the body
classNamestring—Extra classes for the root

Type Exports

TypeDescription
InstantMessageComposerPropsProps for the composer
InstantMessageComposerVariant'default' | 'outline' | 'minimal'
InstantMessageComposerSize'sm' | 'default' | 'lg'
InstantMessageChannel'sms' | 'whatsapp'
InstantMessageAttachment{ name: string; size: number }

Recipe: Sending to a contact selection

Pull recipients out of a row selection (data grid, list, etc.) and feed them into the composer as the initial chip list. Treat the controlled recipients prop as the source of truth so users can still tweak the chips in the dialog before pressing Send.

const selectedContacts = useSelectedContacts();
const phones = useMemo(
  () => Array.from(new Set(
    selectedContacts.map(c => c.mobile?.trim()).filter((p): p is string => Boolean(p))
  )),
  [selectedContacts]
);

const [recipients, setRecipients] = useState(phones);
const [body, setBody] = useState('');
const [channel, setChannel] = useState<InstantMessageChannel>('sms');

return (
  <InstantMessageComposer
    variant="minimal"
    channel={channel}
    onChannelChange={setChannel}
    recipients={recipients}
    onRecipientsChange={setRecipients}
    body={body}
    onBodyChange={setBody}
    onSend={() => sendMessage({ channel, recipients, body })} />
);

On this page