Quote reply
Reply to part of a message. Select text in an answer and a small Reply and Copy toolbar appears; Reply puts the excerpt above the composer as a chip, and the sent message carries it as a quiet block above the text.
On this page
Installation
Usage
import { type ChatcnQuote, QuoteReply, withQuote } from "@/components/ui/chatcn/quote-reply"
const [quote, setQuote] = React.useState<ChatcnQuote | null>(null)
<Message from="assistant">
<QuoteReply.Trigger messageId={message.id} onReply={(q) => { setQuote(q); composer.current?.focus() }}>
<MessageContent>{text}</MessageContent>
</QuoteReply.Trigger>
</Message>
<PromptInput onSubmit={({ text }) => { send(withQuote(text, quote)); setQuote(null) }}>
{quote ? (
<PromptInputHeader className="px-2 pt-2">
<QuoteReply.Chip quote={quote} onRemove={() => setQuote(null)} />
</PromptInputHeader>
) : null}
<PromptInputTextarea ref={composer} />
</PromptInput>Three pieces, also exported flat as QuoteReplyTrigger, QuoteReplyChip and QuoteReplyBlock. The trigger wraps any content and listens to selections inside it only, so selecting in the composer or the sidebar never brings up the toolbar. withQuote(text, quote) prepends the excerpt as a Markdown blockquote, which is what models expect.
Show the quote on the sent message by keeping it next to the text, for example in message metadata, and render it with QuoteReply.Block before MessageContent.
<Message from="user">
{quote ? <QuoteReply.Block quote={quote} onSelect={() => scrollToMessage(quote.messageId)} /> : null}
<MessageContent>{text}</MessageContent>
</Message>Examples
Quote chip
The chip clamps to two lines and shows a toggle only when the quote runs longer. Placed in PromptInputHeader with 8 px padding, its 8 px radius sits concentric inside the 16 px composer. Backspace in an empty composer is a good second way to remove it, as the demo shows.
Quote on a sent message
One step down in gray and 13 px, aligned with the message: the end edge for user bubbles. The full quote stays in the DOM, so screen readers hear all of it even when two lines show.
Props
QuoteReply.Trigger
| Prop | Type | Notes |
|---|---|---|
onReply | (quote: ChatcnQuote) => void | Required. Show the chip and focus the composer |
onCopy | (text: string) => void | After Copy. Copy confirms in place either way |
messageId | string | Carried on the quote, for jump links |
copy | boolean | Show Copy next to Reply. Defaults to true |
replyLabel | string | Defaults to "Reply" |
minLength | number | Ignore shorter selections. Defaults to 2 |
maxLength | number | Longer quotes end with an ellipsis. Defaults to 1200 |
disabled | boolean | Stop listening to selections |
QuoteReply.Chip
| Prop | Type | Notes |
|---|---|---|
quote | ChatcnQuote | string | Required |
onRemove | () => void | Shows the remove button |
lines | 1 | 2 | 3 | Lines before the toggle. Defaults to 2 |
expanded, defaultExpanded, onExpandedChange | boolean | Controlled or uncontrolled |
QuoteReply.Block
| Prop | Type | Notes |
|---|---|---|
quote | ChatcnQuote | string | Required |
onSelect | (quote) => void | Makes the block a button that jumps to the source |
lines | 1 | 2 | 3 | Defaults to 2 |
Helpers
| Export | Notes |
|---|---|
createQuote(text, options) | Trims, collapses whitespace and caps the length. Returns a ChatcnQuote |
withQuote(text, quote) | The message text with the quote as a > blockquote in front |
Keyboard
| Key | Action |
|---|---|
Shift + arrows | Select text with caret browsing (F7) or a screen reader. The toolbar appears without animation |
Tab | From the selected text, move into the toolbar |
ArrowLeft, ArrowRight | Move between Reply and Copy |
Enter, Space | Run the focused action |
Esc | Close the toolbar and return to the text with the selection intact |
Backspace | In an empty composer, remove the quote (wire it as the demo does) |
Accessibility
- The toolbar is a
toolbarwith one tab stop. A keyboard selection announces Text selected. Press Tab for actions. - The chip is a group named Replying to. Its toggle uses
aria-expandedandaria-controls, and the remove button is named Remove quote. - The block reads as Replying to, then the quote. With
onSelectit is a button named Jump to quoted message. - Every button grows to a 44 px hit area on touch screens.
Motion
- A pointer selection brings the toolbar in over 150 ms: opacity and 4 px toward the text. A keyboard selection shows it instantly and it follows Shift and the arrows without moving.
- The chip eases in over 150 ms when Reply was clicked, and appears at once when Reply was chosen from the keyboard. Under reduced motion it only fades.
- Expanding the chip is instant: it is a text clamp, not a panel.