Super-AI-Components

Quote Reply

Select content → quoted block in composer

the model needs the negative prompt to stay in sync with the reference strip

Text · ¶4
the torn edge of the poster
Image · 212,88 · 160×120

$42,300

Table cell · Q3 Budget!C12

so that's the part we want to cut

Timeline · 0:42–0:51

What it is

A small quoted block that attaches a selection — a text range, an image region, a table cell, or a timeline range — to the message being composed. It shows a readable excerpt of what was selected, where that selection came from, and a control to drop the quote without touching anything already typed.

Why it matters

Claude, Notion AI, Spellbook, and Descript all let a user select something first and reply to that selection specifically, across four completely different surfaces — a document, an image, a spreadsheet, a transcript. The pattern only holds together because it's one component, not four: the excerpt is the only thing that changes shape per source, while the anchor, the source-kind glyph, and the removal control stay identical. That's what lets a quote outlive the moment it was taken — the anchor is a stable position, not a snapshot of the text, so it can still say where it pointed even after the source underneath it changes.

Observed in: Claude · Notion AI · Spellbook · Descript

Anatomy

  1. 1quote-replyRoot container — one row holding the icon, the excerpt, and the remove control.
  2. 2quote-reply-iconDecorative glyph naming the source kind at a glance; the label carries the same information in text.
  3. 3quote-reply-excerptThe <blockquote> — a text excerpt for three sources, a thumbnail for image-region.
  4. 4quote-reply-textThe excerpt text itself, when the source isn't an image with a thumbnail.
  5. 5quote-reply-anchorThe <cite> line naming the source kind and the stable anchor — independent of the excerpt text.
  6. 6quote-reply-removeRemoves the quote. Never touches the composer's typed draft.

How to use it

Reach for it whenever a composer needs to carry a reply-to-this-selection context alongside the typed message — always pass a real `anchor` (a paragraph offset, a cell reference, a time range, or region bounds), not a restatement of the excerpt, since the anchor is what the quote resolves against if the source is edited later. Pass `thumbnail` for image-region quotes once you have a crop to show; until then it falls back to the same text rendering as the other three sources, so the component never has to wait on an image to be useful.

Do

  • Pass the excerpt and the anchor as two independent values — the excerpt is what was selected, the anchor is where it lives, and only the anchor has to keep resolving after an edit.

    the negative prompt should stay in sync with the reference strip

    Text · ¶4
  • Mount the quote as a sibling of the composer's input, not inside it — removing the quote must never clear what the user already typed.

    so that's the part we want to cut

    Timeline · 0:42–0:51

Don't

  • Don't quote a whole paragraph or transcript minute — trim to the sentence the reply is actually about. line-clamp-2 will cut a long excerpt mid-sentence, which reads as broken rather than intentional.

    the model needs the negative prompt to stay in sync with the reference strip at all times, otherwise the render drifts from what the team actually approved in the last review pass and someone has to catch it after the fact

    Text · ¶4
  • Don't hand it a vague anchor like "here" — the anchor is the one thing that has to survive the source being edited, so it needs to be an actual position, not a placeholder.

    $42,300

    Table cell · here

Accessibility

Keyboard
  • Zero tab stops without `onRemove`, one with it. The excerpt, the `<cite>` anchor and the source glyph are static text, and an `image-region` quote's thumbnail is a `preview-tile` with no `onSelect` — its frame stays a `<div>`, so the image is not focusable and cannot be opened from the keyboard.
  • The remove control is a real button, so Space and Enter both fire `onRemove`. There is no Delete or Backspace shortcut and no Escape handler — dropping a quote from the keyboard means tabbing to the X.
  • There is no `disabled` prop. Everything beyond the named props spreads onto the root `<div>`, not onto the remove button, so a `disabled` or `tabIndex` you pass lands on a container that was never focusable and the X stays live.
Screen reader
  • The root is a plain `<div>` with no role and no name. Several quotes stacked above one composer announce as loose blocks of text, with nothing grouping them and nothing saying how many are attached.
  • The excerpt is a `<blockquote>`, so it announces as a quote, and the `<cite>` reads straight after it as "Text · <anchor>" — the source word comes from `source`, the rest is whatever you passed as `anchor`.
  • `line-clamp-2` clips the excerpt visually only. The whole string stays in the accessible text, so a screen-reader user hears a full paragraph where a sighted user sees two lines and an ellipsis.
  • The remove button's name is `removeLabel`, which defaults to "Remove quote" on every instance. Three quotes above one composer give three buttons with identical names; pass `removeLabel` naming the source instead.
  • The source glyph is `aria-hidden`, so which of the four sources this is gets announced only through the word in the `<cite>` line.
  • For `image-region`, `excerpt` becomes `preview-tile`'s below-placed label and is announced as ordinary text after the image. The `thumbnail` node is yours — an `<img>` without `alt` announces as an unnamed image immediately before that caption.
  • Nothing announces that a quote was attached or dropped. There is no live region here, so if the attachment needs confirming, put one on the composer.
Focus
  • Removing a quote unmounts the button that had focus and nothing restores it, so focus falls to `<body>` and the next Tab restarts from the top of the page. Move focus to the composer's input inside your `onRemove`.
  • The remove button is the only focusable element and it inherits the shared `Button` focus ring, so it is visible on focus with no work at the call site.

Watch out for

  • Four sources, one component — resist forking text-range/image-region/table-cell/timeline-range into four separate components. Only the excerpt rendering branches internally (thumbnail vs. text); the icon, the anchor line, and the removal control are identical across all four, and a fork is how one of them quietly loses the dismiss control or the anchor contract.
  • For image-region, excerpt is the visible caption under the thumbnail, not just an accessibility label — leaving it as a placeholder string makes the quote unreadable at a glance even though a screen reader would still announce something.
  • thumbnail is optional on purpose: without it, an image-region quote renders through the exact same text branch as the other three sources. Don't assume the thumbnail branch always fires for source="image-region" — design for the text fallback too.

Installation

npx shadcn@latest add https://super-ai-components.vercel.app/r/quote-reply.json

Requires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.