Reply draft
ProAn AI email reply drafter where Claude streams a reply under a received Gmail message. Quick replies steer a fresh draft, tone and length rewrite it with a single crossfade, and you can edit the finished draft before you insert it.
Live · keyboard ready
- Drafting a reply to an email, ticket or message with an AI model, where the reader reviews and edits before sending.
- Offering quick reply intents plus tone and length controls for generated text.
- Showing an AI writing feature on a landing page with a realistic, self-playing example.
- Free-form chat with a model: use a chat or composer component instead.
- Inline autocomplete while typing: use ghost-text.
- Sending the email itself: this component inserts a draft; your composer owns sending.
Installation
Pro source and install commands unlock with a Pro plan.
example.tsx
import { ReplyDraft } from "@/components/arc/reply-draft/reply-draft"; export function Inbox({ email }) { return ( <ReplyDraft message={email} suggestions={[{ id: "yes", label: "Sounds good" }, { id: "later", label: "Next week" }]} model={{ name: "Claude Sonnet 5.5" }} generate={async function* ({ message, tone, length, suggestion, signal }) { const res = await fetch("/api/reply", { method: "POST", body: JSON.stringify({ id: email.id, tone, length, hint: suggestion?.label }), signal }); const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader(); for (;;) { const { value, done } = await reader.read(); if (done) return; yield value; } }} onInsert={draft => composer.insert(draft)} /> );}ReplyDraft
A received email with an AI reply under it: quick reply chips, a fast streaming draft, tone and length rewrites, Regenerate and Insert. With no props it plays a simulated tour.
PropTypeDefaultDescription
messageReplyDraftMessagereplyDraftSampleMessageThe received email: from { name, email, avatar? }, to?, subject, body (blank lines split paragraphs) and receivedAt.suggestionsReplyDraftSuggestion[]3 sample quick repliesQuick replies ({ id, label }) shown as chips above the draft. Selecting one streams a fresh draft steered by it. Pass [] to hide the row.defaultSuggestionstring–Id of the quick reply that steers the first draft. Defaults to the first suggestion.onSuggestionSelect(suggestion: ReplyDraftSuggestion) => void–Called when a quick reply chip is chosen.modelReplyDraftModel{ name: "Claude Sonnet 5.5" }Model name and an optional 16px mark shown on the draft. The Claude mark is the default.tone"friendly" | "concise" | "formal"–Controlled tone. Changing it rewrites a finished draft with a crossfade, or restarts one that is still streaming.defaultTone"friendly" | "concise" | "formal""friendly"Initial tone when uncontrolled.onToneChange(tone: ReplyDraftTone) => void–Called when the reader picks a tone.length"short" | "medium" | "long"–Controlled length. Works like tone.defaultLength"short" | "medium" | "long""medium"Initial length when uncontrolled.onLengthChange(length: ReplyDraftLength) => void–Called when the reader picks a length.generate(request: ReplyDraftRequest) => Promise<string> | AsyncIterable<string>simulateReplyDraftWrites the draft. The request carries message, tone, length, suggestion, mode ("draft" or "rewrite"), previous (the current text with edits) and an AbortSignal. Return the whole text, which is streamed locally in chunks, or yield chunks from your model stream. Throw to show the error state.onInsert(draft: string, meta: { tone, length, suggestion? }) => void | Promise<void>–Receives the final draft, including the reader's edits. While a returned promise is pending, Insert shows a spinner. Without it, Insert is simulated and announces that nothing was sent.onDraft(draft: string, meta: { tone, length, suggestion? }) => void–Called each time a draft or rewrite finishes.autoStartbooleantrueStart the first draft when the card scrolls into view. When false, the draft box shows an empty state with a Draft reply button.autoPlaybooleantrue without generateTours tones, length, a second quick reply and Insert on a loop. Stops for good on the first pointer or key press inside the card and pauses off screen.classNamestring–Class on the outer wrapper (max width 600px, fills narrower containers).- Tab
- Moves through quick replies, the draft text, tone, length, Regenerate and Insert.
- Arrow keys
- Inside Tone or Length, move the selection and rewrite the draft. Home and End jump to the first and last option.
- EnterorSpace
- Activates a quick reply or button.
- CmdorCtrl+Enter
- Inserts the finished draft from anywhere inside the card.
- The card is an article labelled with the sender; the reply area is a labelled section.
- Quick replies are toggle buttons with aria-pressed; tone and length are radio groups with roving tabindex.
- The finished draft is a plain-text editable textbox (contenteditable plaintext-only), so readers can fix a word before Insert. Edits are what onInsert receives.
- A polite live region announces drafting, rewriting, ready, inserted and errors. Simulated actions say nothing was sent.
- The body is aria-busy while drafting. Status uses an icon plus a short label, never color alone.
- Drafts stream in chunks of 3 to 8 words every 34ms; each chunk only fades in over 140ms.
- Tone and length rewrites crossfade once: the old draft dims during the short rewrite, fades out in 100ms and the new one fades in over 160ms with a 2px rise.
- The draft box springs to its measured height with no bounce; during a regenerate its height is held until the new draft finishes, so the card never jumps.
- The selected tone and length pills glide with a bounce-free spring. Status labels crossfade; working labels use a calm shimmer.
- Reduced motion: drafts appear whole, crossfades and the height spring are instant, chunk fades and the press scale are dropped.
- Fills its container up to 600px. Below 480px the card padding drops to 16px and the email body loses its avatar indent.
- Quick replies and the tone and length groups wrap; under 460px the divider between the groups hides and Length moves to its own row.
- Touch targets grow to 32px for options and 36px for quick replies on coarse pointers. Long emails and names truncate or wrap without horizontal scroll.
- One state update per streamed chunk; chunks are plain spans with a CSS fade, swapped for a single text node when the draft finishes.
- The autoplay loop and auto start use an IntersectionObserver and stay idle off screen.
- No network calls by default; the simulation lives in reply-draft-data.ts.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use for AI-assisted replies in inboxes, support desks, CRMs and chat tools. Pass the real message and wire generate to your model with the AbortSignal; every new action aborts the previous request.
- generate can return a Promise<string> (it is chunk-streamed locally) or an async iterable of text deltas from a real stream. mode "rewrite" means restyle the previous text for the new tone or length.
- Map suggestions to intents your prompt understands (ids are stable, labels are what readers see). Keep labels to two or three words.
- Leave autoPlay off in products; it is for marketing and docs demos.
The full library index for assistants is at /llms.txt.
