# Quote Selected Text
URL: /docs/guides/quoting

Let users select and quote text from messages, similar to Claude's quoting experience.

Allow users to select text in assistant messages and reply with a quote reference — just like Claude, ChatGPT, and other modern AI interfaces.

How It Works \[#how-it-works]

1. User selects text in a message
2. A floating toolbar appears near the selection with a **Quote** button
3. User clicks it — a quote preview appears in the composer
4. User types their reply and sends
5. The sent message displays the quoted text above the user's reply

Quote data is stored in `metadata.custom.quote` and is **not** injected into message content. Your backend decides how to present the quoted context to the LLM.

Quick Start \[#quick-start]

1. Add the Floating Selection Toolbar \[#1-add-the-floating-selection-toolbar]

Place `SelectionToolbarPrimitive` inside your `ThreadPrimitive.Root`. It renders a floating toolbar near the user's text selection — only when text is selected within a message.

```tsx {1,13-20}
import { SelectionToolbarPrimitive, ThreadPrimitive } from "@assistant-ui/react";
import { QuoteIcon } from "lucide-react";

const Thread = () => {
  return (
    <ThreadPrimitive.Root>
      <ThreadPrimitive.Viewport>
        <ThreadPrimitive.Messages components={{ ... }} />
        ...
      </ThreadPrimitive.Viewport>

      {/* Floating toolbar — appears on text selection */}
      <SelectionToolbarPrimitive.Root className="flex items-center gap-1 rounded-lg border bg-popover px-1 py-1 shadow-md">
        <SelectionToolbarPrimitive.Quote className="flex items-center gap-1.5 rounded-md px-2.5 py-1 text-sm hover:bg-accent">
          <QuoteIcon className="size-3.5" />
          Quote
        </SelectionToolbarPrimitive.Quote>
      </SelectionToolbarPrimitive.Root>
    </ThreadPrimitive.Root>
  );
};
```

The `Root` component:

* Listens for `mouseup` and `keyup` events to detect text selections
* Validates the selection is within a single message (cross-message selections are ignored)
* Renders a portal positioned above the selection
* Prevents `mousedown` from clearing the selection when clicking the toolbar
* Hides automatically on scroll or when the selection is cleared

2. Show Quote Preview in Composer \[#2-show-quote-preview-in-composer]

Add `ComposerPrimitive.Quote` inside the composer to show what's being quoted:

```tsx {1,8-13}
import { ComposerPrimitive } from "@assistant-ui/react";

const Composer = () => {
  return (
    <ComposerPrimitive.Root>
      {/* Quote preview — only renders when a quote is set */}
      <ComposerPrimitive.Quote className="flex items-start gap-2 border-l-4 border-primary/40 bg-muted/50 px-3 py-2 text-sm">
        <ComposerPrimitive.QuoteText className="line-clamp-2 flex-1 italic text-muted-foreground" />
        <ComposerPrimitive.QuoteDismiss>
          ×
        </ComposerPrimitive.QuoteDismiss>
      </ComposerPrimitive.Quote>

      <ComposerPrimitive.Input placeholder="Send a message..." />
      <ComposerPrimitive.Send />
    </ComposerPrimitive.Root>
  );
};
```

3. Display Quotes in Sent Messages \[#3-display-quotes-in-sent-messages]

Use `useMessageQuote()` to render quoted text in user messages:

```tsx {1,4-5,11}
import { MessagePrimitive, useMessageQuote } from "@assistant-ui/react";

const QuoteBlock = () => {
  const quote = useMessageQuote();
  if (!quote) return null;

  return (
    <div className="mb-1 border-l-4 border-primary/40 pl-3 text-xs italic text-muted-foreground">
      {quote.text}
    </div>
  );
};

const UserMessage = () => {
  return (
    <MessagePrimitive.Root>
      <QuoteBlock />
      <MessagePrimitive.Parts />
    </MessagePrimitive.Root>
  );
};
```

Backend Handling \[#backend-handling]

The quote is stored in message metadata — **not** in message content. This gives your backend full control over how to present quoted context to the LLM.

Data Shape \[#data-shape]

```typescript
type QuoteInfo = {
  readonly text: string;       // selected plain text
  readonly messageId: string;  // source message ID
};

// Stored at: message.metadata.custom.quote
```

Example: Prepend as Markdown Blockquote \[#example-prepend-as-markdown-blockquote]

A simple approach is to prepend the quoted text as a `>` blockquote before converting to model messages:

```typescript title="app/api/chat/route.ts" {1,9}
import { convertToModelMessages, streamText } from "ai";
import type { UIMessage } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: myModel,
    messages: await convertToModelMessages(injectQuoteContext(messages)),
  });

  return result.toUIMessageStreamResponse();
}

function injectQuoteContext(messages: UIMessage[]): UIMessage[] {
  return messages.map((msg) => {
    const quote = (msg.metadata as Record<string, unknown>)?.custom;
    if (
      !quote ||
      typeof quote !== "object" ||
      !("quote" in (quote as Record<string, unknown>))
    )
      return msg;

    const q = (quote as Record<string, unknown>).quote;
    if (
      !q ||
      typeof q !== "object" ||
      !("text" in (q as Record<string, unknown>))
    )
      return msg;

    const text = (q as { text: unknown }).text;
    if (typeof text !== "string") return msg;

    return {
      ...msg,
      parts: [{ type: "text" as const, text: `> ${text}\n\n` }, ...msg.parts],
    };
  });
}
```

Example: Claude-Style Citation Source \[#example-claude-style-citation-source]

For Claude's API, you can pass the quoted text as a citation source. This enables Claude to produce citations that reference the quoted text:

```typescript title="app/api/chat/route.ts"
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

export async function POST(req: Request) {
  const { messages } = await req.json();

  // Transform messages: extract quotes into Claude source blocks
  const claudeMessages = messages.map((msg) => {
    const quote = msg.metadata?.custom?.quote;
    if (!quote?.text) {
      return { role: msg.role, content: extractText(msg) };
    }

    return {
      role: "user",
      content: [
        {
          type: "text",
          text: quote.text,
          cache_control: { type: "ephemeral" },
          citations: { enabled: true },
        },
        {
          type: "text",
          text: extractText(msg),
        },
      ],
    };
  });

  const response = await client.messages.create({
    model: "claude-sonnet-4-5-20250929",
    max_tokens: 1024,
    messages: claudeMessages,
  });

  // ... stream response back
}
```

Example: OpenAI-Style System Context \[#example-openai-style-system-context]

For OpenAI's API, inject the quote as additional context in the user message:

```typescript title="app/api/chat/route.ts"
function injectQuoteForOpenAI(messages) {
  return messages.map((msg) => {
    const quote = msg.metadata?.custom?.quote;
    if (!quote?.text || msg.role !== "user") return msg;

    return {
      ...msg,
      content: `[Referring to: "${quote.text}"]\n\n${msg.content}`,
    };
  });
}
```

Programmatic API \[#programmatic-api]

You can set or clear quotes programmatically via the composer runtime:

```tsx
import { useAui } from "@assistant-ui/react";

function MyComponent() {
  const aui = useAui();

  const quoteText = () => {
    aui.thread().composer.setQuote({
      text: "The text to quote",
      messageId: "msg-123",
    });
  };

  const clearQuote = () => {
    aui.thread().composer.setQuote(undefined);
  };

  return (
    <>
      <button onClick={quoteText}>Set Quote</button>
      <button onClick={clearQuote}>Clear Quote</button>
    </>
  );
}
```

API Reference \[#api-reference]

SelectionToolbarPrimitive.Root \[#selectiontoolbarprimitiveroot]

A floating container that appears when text is selected within a message. Renders as a portal positioned above the selection.

* Listens for `mouseup` / `keyup` to detect selection
* Validates selection is within a single message
* Hides on scroll or when selection is cleared
* Prevents `mousedown` from clearing the selection
* Provides selection context to child components

SelectionToolbarPrimitive.Quote \[#selectiontoolbarprimitivequote]

A button inside the floating toolbar that captures the selection as a quote.

* Reads selection info from the toolbar context (not `window.getSelection()`)
* Stores `{ text, messageId }` in the thread composer
* Clears the text selection after quoting

ComposerPrimitive.Quote \[#composerprimitivequote]

A container that only renders when a quote is set.

ComposerPrimitive.QuoteText \[#composerprimitivequotetext]

Renders the quoted text. Defaults to `<span>`.

ComposerPrimitive.QuoteDismiss \[#composerprimitivequotedismiss]

A button that clears the quote by calling `setQuote(undefined)`. Supports `asChild`.

useMessageQuote() \[#usemessagequote]

```tsx
const quote: QuoteInfo | undefined = useMessageQuote();
```

Returns the quote attached to the current message, or `undefined`.

ComposerRuntime.setQuote() \[#composerruntimesetquote]

```tsx
setQuote(quote: QuoteInfo | undefined): void
```

Set or clear the quote on the composer. The quote is automatically cleared when the message is sent.

Design Notes \[#design-notes]

* **Single quote** — `setQuote` replaces, not appends. Only one quote at a time.
* **Snapshot text** — The selected text is captured at quote time, not linked to the source message.
* **Cross-message selection** — Rejected. The toolbar only appears when the selection is within a single message.
* **Streaming messages** — The floating toolbar works during streaming since it reads from the captured selection, not message status.
* **`isEmpty` unchanged** — A quote alone doesn't make the composer non-empty. The user must type a reply.
* **Scroll hides toolbar** — The floating toolbar hides when any scroll event occurs, since the position would become stale.

Related \[#related]

* [Message Editing](/docs/guides/editing) — Edit user messages
* [Thread Component](/docs/ui/thread) — Main chat container
* [ComposerPrimitive](/docs/reference/primitives/Composer) — Composer primitive reference
* [ActionBarPrimitive](/docs/reference/primitives/ActionBar) — Action bar primitive reference
