# Instant Message Composer
URL: /docs/web/components/instant-message-composer
Composer for SMS and WhatsApp with channel switching, recipient chips, attachments, and SMS character counting.
**Demo:**
```tsx
'use client';
import { useCallback, useState } from 'react';
import {
InstantMessageComposer,
type InstantMessageAttachment,
type InstantMessageChannel,
type InstantMessageComposerSize,
type InstantMessageComposerVariant
} from '@docyrus/ui/components/instant-message-composer';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@docyrus/ui/primitives/ui/select';
import { Switch } from '@docyrus/ui/primitives/ui/switch';
import { cn } from '@docyrus/ui/primitives/lib/utils';
const SAMPLE_BODIES: Record
Size
Channel
);
}
```
## Installation
```bash
pnpm dlx @docyrus/cli add @docyrus/ui-instant-message-composer
```
**Dependencies:**
- [lucide-react](https://www.npmjs.com/package/lucide-react)
- [class-variance-authority](https://www.npmjs.com/package/class-variance-authority)
## Usage
```tsx
import {
InstantMessageComposer,
type InstantMessageChannel
} from "@docyrus/ui/components/instant-message-composer";
{}}
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.
```tsx
// Lock to WhatsApp only
```
### 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` | `['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.
```tsx
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('sms');
return (
sendMessage({ channel, recipients, body })} />
);
```