Skip to content
Sign in
ConversationQuote replyMenu

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.

Select part of the answer and choose Reply. The quote sits above the composer and rides along with the message.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/quote-reply.json

Usage

tsx
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.

tsx
<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.

Short and long quotes in the composer header. Long quotes clamp to two lines with a toggle.

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.

One quiet line or two above the text, aligned with the message. With onSelect it jumps back to the source.

Props

QuoteReply.Trigger

PropTypeNotes
onReply(quote: ChatcnQuote) => voidRequired. Show the chip and focus the composer
onCopy(text: string) => voidAfter Copy. Copy confirms in place either way
messageIdstringCarried on the quote, for jump links
copybooleanShow Copy next to Reply. Defaults to true
replyLabelstringDefaults to "Reply"
minLengthnumberIgnore shorter selections. Defaults to 2
maxLengthnumberLonger quotes end with an ellipsis. Defaults to 1200
disabledbooleanStop listening to selections

QuoteReply.Chip

PropTypeNotes
quoteChatcnQuote | stringRequired
onRemove() => voidShows the remove button
lines1 | 2 | 3Lines before the toggle. Defaults to 2
expanded, defaultExpanded, onExpandedChangebooleanControlled or uncontrolled

QuoteReply.Block

PropTypeNotes
quoteChatcnQuote | stringRequired
onSelect(quote) => voidMakes the block a button that jumps to the source
lines1 | 2 | 3Defaults to 2

Helpers

ExportNotes
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

KeyAction
Shift + arrowsSelect text with caret browsing (F7) or a screen reader. The toolbar appears without animation
TabFrom the selected text, move into the toolbar
ArrowLeft, ArrowRightMove between Reply and Copy
Enter, SpaceRun the focused action
EscClose the toolbar and return to the text with the selection intact
BackspaceIn an empty composer, remove the quote (wire it as the demo does)

Accessibility

  • The toolbar is a toolbar with 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-expanded and aria-controls, and the remove button is named Remove quote.
  • The block reads as Replying to, then the quote. With onSelect it 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.