Elements

17 / 37 · Knowledge

Inline citation

Numbered references inside a sentence, each with a hover preview of its source.

Optimistic updates keep the thread responsive while the server confirms the write. The store already exposes a consistent snapshot for every subscriber, so no extra reconciliation pass is needed.

Installation

npx shadcn@latest add "@assistant-ui/elements-inline-citation"

Usage

import { InlineCitation } from "@/components/elements/inline-citation";

<InlineCitation
  sources={[
    {
      domain: "docs.example.com",
      title: "Optimistic updates",
      snippet: "Confirm writes after paint.",
    },
  ]}
  openIndex={0}
  onOpenIndexChange={setOpenIndex}
/>

Props

sources*Source[]Sources attached to the numbered reference markers in the sentence.
openIndex*number | nullWhich citation preview is open, or null when none is open.
onOpenIndexChange*(index: number | null) => voidCalled when a citation marker opens or closes its preview.
classNamestringExtra classes merged onto the root.

Source

inline-citation.tsx
"use client";

import { PreviewCard } from "@base-ui/react/preview-card";
import { cn } from "@/lib/utils";
import { floating, mono } from "./surfaces";

export interface Source {
  domain: string;
  title: string;
  snippet: string;
}

interface CitationProps {
  index: number;
  source: Source;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

function Citation({ index, source, open, onOpenChange }: CitationProps) {
  return (
    <PreviewCard.Root open={open} onOpenChange={onOpenChange}>
      <PreviewCard.Trigger
        delay={0}
        render={<button type="button" />}
        className={cn(
          "mx-0.5 inline-flex h-4 min-w-4 translate-y-[-2px] cursor-default items-center justify-center rounded-[5px] px-1 align-middle font-mono text-[10px] font-medium tabular-nums transition-colors",
          open
            ? "bg-foreground text-background"
            : "bg-foreground/[0.06] text-foreground/45 hover:text-foreground/90",
        )}
      >
        {index + 1}
      </PreviewCard.Trigger>
      <PreviewCard.Portal>
        <PreviewCard.Positioner side="top" sideOffset={8}>
          <PreviewCard.Popup
            className={cn(
              floating,
              "z-50 w-64 origin-(--transform-origin) rounded-2xl p-3.5 outline-none",
              "transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
              "data-[starting-style]:scale-[0.97] data-[starting-style]:opacity-0",
              "data-[ending-style]:scale-[0.97] data-[ending-style]:opacity-0",
            )}
          >
            <div className="flex items-center gap-1.5">
              <span className="bg-foreground/[0.06] text-foreground/45 flex size-4 items-center justify-center rounded text-[9px] font-medium">
                {source.domain[0]?.toUpperCase()}
              </span>
              <span className={cn(mono, "text-foreground/40")}>
                {source.domain}
              </span>
            </div>
            <p className="mt-2 text-[13px] leading-snug font-medium">
              {source.title}
            </p>
            <p className="text-foreground/50 mt-1 text-[13px] leading-relaxed">
              {source.snippet}
            </p>
          </PreviewCard.Popup>
        </PreviewCard.Positioner>
      </PreviewCard.Portal>
    </PreviewCard.Root>
  );
}

export interface InlineCitationProps {
  sources: Source[];
  openIndex: number | null;
  onOpenIndexChange: (index: number | null) => void;
  className?: string;
}

export function InlineCitation({
  sources,
  openIndex,
  onOpenIndexChange,
  className,
}: InlineCitationProps) {
  return (
    <p
      className={cn(
        "text-foreground/90 max-w-sm text-sm leading-relaxed",
        className,
      )}
    >
      Optimistic updates keep the thread responsive while the server confirms
      the write
      {sources[0] && (
        <Citation
          index={0}
          source={sources[0]}
          open={openIndex === 0}
          onOpenChange={(open) => onOpenIndexChange(open ? 0 : null)}
        />
      )}
      . The store already exposes a consistent snapshot for every subscriber
      {sources[1] && (
        <Citation
          index={1}
          source={sources[1]}
          open={openIndex === 1}
          onOpenChange={(open) => onOpenIndexChange(open ? 1 : null)}
        />
      )}
      , so no extra reconciliation pass is needed.
    </p>
  );
}