Email Draft
ProThe email an assistant wrote for you, shown as an email instead of a blob of JSON. You read who it goes to, what it says and what is attached, fix it in place, and send it. Once it is gone it folds into a single line you can open again.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
A sheet with the email on it: an address block, the subject, the body and the files. Under it, the actions. The root adds the summary line the draft folds into once it is sent or discarded, so you never render it yourself.
import {
EmailDraft,
EmailDraftContent,
EmailDraftHeader,
EmailDraftField,
EmailDraftRecipients,
EmailDraftRecipient,
EmailDraftSubject,
EmailDraftBody,
EmailDraftAttachments,
EmailDraftActions,
} from "@/components/ui/email-draft"
<EmailDraft>
<EmailDraftContent>
<EmailDraftHeader>
<EmailDraftField>
<EmailDraftRecipients>
<EmailDraftRecipient />
</EmailDraftRecipients>
</EmailDraftField>
</EmailDraftHeader>
<EmailDraftSubject />
<EmailDraftBody />
<EmailDraftAttachments />
</EmailDraftContent>
<EmailDraftActions />
</EmailDraft>Draft#
The resting state: the email as it will arrive. Recipients are chips with a face and a name, the address one hover away. A long Cc list caps itself with max and the rest sit behind a +N that names them on hover and opens them on click. The body keeps every line break the model wrote. Discard sits apart on the left, in the destructive ink but without a fill; Edit and Send pair up on the right.
Editing#
Edit turns the subject into an Input and the body into an autosizing Textarea, drawn around the words where they already are: the fields grow out into the margin, so not a letter moves. The body takes focus with the caret at the end. Edit and Send roll into Cancel and Done; Escape cancels and puts the text back, Done (or ⌘ Enter) keeps it.
Sending#
Return a promise from onSend and the draft waits on it: Send holds a spinner at its own width, Edit and Discard step back, and the root is aria-busy. A rejected promise drops back to the draft, with the edits intact.
Sent#
Once it goes, the draft folds into one line on the same clock as the actions leave: who it went to, and when. View opens the email again where it was, read only, and Hide folds it back. The time is stamped when it sends; pass sentAt to show your server's instead.
Discarded#
Discard folds the draft the same way, into a muted line with an Undo, so a slip costs one click rather than a new prompt.
Attachments#
EmailDraftAttachments lays out Koala's Attachment under the body, so a file in a draft looks exactly like a file in the composer or a sent message. Give each one an onOpen or an href to make it open.
With Tool approval#
When the email is a tool call waiting for a yes, drop EmailDraftActions and let the Tool's approval row decide. Use variant="plain" so the sheet sits in the Tool's frame like any other input, drive editing from the approval's Edit, and map the call's status: running is sending, done is sent, rejected is discarded.
Send this email to Dana Whitfield?
Variants#
card sets the sheet into a tinted frame with the actions on the frame, the same inset look as Tool and Approval. plain is the sheet alone, for a surface that already frames it.
Density#
Density tunes the sheet's vertical rhythm only. The side margin stays at 16px in both, because that is the room the edit fields grow into.
API reference#
EmailDraft
The draft, and the context every part reads. Renders the folded summary line itself and wraps its children in the fold. Every part accepts className, merged last, and every part is droppable.
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "draft" | "sending" | "sent" | "discarded" | - | Controlled status. sent and discarded fold the draft into one line. |
| defaultStatus | EmailDraftStatus | "draft" | Initial status when uncontrolled. |
| onStatusChange | (status: EmailDraftStatus) => void | - | Called on Send, Discard, Undo, and when a send settles. |
| onSend | (edits: EmailDraftEdits) => void | Promise<unknown> | - | Called with the fields the person changed. Return a promise and the draft holds sending until it resolves (then sent) or rejects (back to draft); return nothing and it goes straight to sent. |
| onDiscard | () => void | - | Called when the draft is discarded. |
| editing | boolean | - | Controlled edit mode. Only takes effect while the status is draft. |
| defaultEditing | boolean | false | Initial edit mode when uncontrolled. |
| onEditingChange | (editing: boolean) => void | - | Called on Edit, Done, Cancel and Escape. |
| onEditsChange | (edits: EmailDraftEdits) => void | - | Called on every keystroke with { subject?, body? }, and with the restored values when an edit is cancelled. |
| recipient | string | - | Who the folded line names. Defaults to the first recipient in the to field, and counts the rest. |
| sentAt | Date | string | - | The time on the sent line. A string is shown as is; a Date is formatted on the client. Stamped automatically when it sends. |
| variant | "card" | "plain" | "card" | card sets the sheet in a tinted frame; plain is the sheet alone, for a Tool or a panel that already frames it. |
| density | "comfortable" | "compact" | - | The sheet's vertical padding. Inherits the nearest DensityProvider (compact with none). |
| Data attribute | Description |
|---|---|
| data-status | The current status. |
| data-editing | Present while editing. |
| data-folded | Present when sent or discarded. |
| data-collapsed | Present while the message is folded away (sent and not viewed, or discarded). |
EmailDraftContent
The sheet the email is written on: the header, subject, body and attachments. Forwards native <div> props.
EmailDraftHeader
The address block. Draws a hairline under itself that disappears when nothing follows. Forwards native <div> props.
EmailDraftField
One address row: a label column and its value. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| kind | "from" | "to" | "cc" | "bcc" | - | Names the row (From, To, Cc, Bcc). Recipients in the to row are the ones the folded line names. |
| label | React.ReactNode | - | Overrides the label kind picks. |
| Data attribute | Description |
|---|---|
| data-kind | The row's kind. |
EmailDraftRecipients
A list of recipient chips that can cap itself. Forwards native <span> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| max | number | - | Chips shown before the rest collapse into a +N button. The button lists the hidden names on hover and shows them all on click. |
EmailDraftRecipient
One person: avatar and name in a chip, with the address in a tooltip and in the accessible name. Forwards native <span> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | - | The display name. |
| string | - | The address. Shown alone when there is no name. | |
| src | string | - | The avatar photo. Initials stand in while it loads or when there is none. |
EmailDraftSubject
The subject line. Pass it as a string and it turns into an Input while editing, set so the words do not move. Forwards native <div> props.
EmailDraftBody
The message. Line breaks and blank lines are kept as written. Pass it as a string and it turns into an autosizing Textarea while editing, focused at the end. Forwards native <div> props.
EmailDraftAttachments
The files going with it, as Koala Attachments. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "inline" | "default" | Passed to Attachments. |
EmailDraftActions
Discard, Edit and Send. While editing they become Cancel and Done, with Discard stepping out of the way; while sending, Send holds a spinner. Folds away once the draft is sent or discarded. Children land between Discard and Edit. Forwards native <div> props.