Instant Message Composer
Composer for SMS and WhatsApp with channel switching, recipient chips, attachments, and SMS character counting.
Installation
pnpm dlx @docyrus/cli add @docyrus/ui-instant-message-composernpx shadcn@latest add badge button separator tooltipUsage
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
| Variant | Description |
|---|---|
default | Default style with input border and shadow |
outline | Outline border, no shadow |
minimal | No border or shadow — for embedding in other surfaces (dialogs, sheets) |
Sizes
| Size | Description |
|---|---|
sm | Small — compact text and shorter body min-height |
default | Default — comfortable text and 150px min body |
lg | Large — bigger text and taller body min-height |
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "outline" | "minimal" | "default" | Visual style |
size | "sm" | "default" | "lg" | "default" | Sizing scale |
channel | InstantMessageChannel | — | Active channel: 'sms' or 'whatsapp'. Required. |
onChannelChange | (channel: InstantMessageChannel) => void | — | Called when the user switches channels. Omit to lock the channel. |
availableChannels | ReadonlyArray<InstantMessageChannel> | ['sms', 'whatsapp'] | Channels offered in the segmented switcher |
recipients | string[] | — | Recipient phone numbers (chip list). Required. |
onRecipientsChange | (recipients: string[]) => void | — | Called when chips are added/removed/pasted |
body | string | — | 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 |
sending | boolean | false | Shows a spinner on the send button and disables interactions |
disabled | boolean | false | Disables all inputs and actions |
attachments | InstantMessageAttachment[] | — | List of attached files (WhatsApp only) |
onRemoveAttachment | (index: number) => void | — | Called when an attachment chip is removed |
maxLength | number | null | 1600 for SMS, no limit for WhatsApp | Hard cap on body length. Pass null to remove the limit. |
helper | ReactNode | — | Optional helper rendered between the recipients row and the body |
className | string | — | Extra classes for the root |
Type Exports
| Type | Description |
|---|---|
InstantMessageComposerProps | Props 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 })} />
);Image Editor
An image editor with crop, brightness, saturation, contrast, and hue adjustments, supporting rectangle and circle stencils with zoom controls.
Json Schema Designer
A row-based JSON Schema designer ported from extend-hq's schema-builder. Edit property name, type, description and enum values inline; add children directly from each container; preview the live JSON Schema in the side tab.