AI Citation Check

Flags references in AI-written text that look made up and looks each one up, so fake cases and DOIs are caught before they go out.

StopsUnverified references slip into AI text

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/ai-citation-check.json

The files land in your project. They're yours to edit.

Usage

ai-citation-check-usage.tsx
import { AiCitationCheck, type Reference, type VerifyResult } from "@/components/ui/ai-citation-check"

<AiCitationCheck
  id="citation-demo"
  value={text}
  onValueChange={(v) => {
    setText(v)
    setDone(false)
  }}
  year={2026}
  sourceName="Crossref or CourtListener"
  verify={async (ref: Reference) => {
    await new Promise((r) => setTimeout(r, 500 + (ref.start % 5) * 150))
    return KNOWN[ref.key ?? ""] ?? false
  }}
  onSubmit={async () => {
    await new Promise((r) => setTimeout(r, 400))
    setDone(true)
  }}
/>

What it catches

  • DOI prefixes that do not belong to the publisher of the journal named
  • Long dashes inside a DOI and leftover AI citation markers, with one-tap fixes
  • Case reporter/year combinations that cannot exist, and years in the future
  • Long quotes with no attribution
  • References your verify function could not confirm, shown as Unverified rather than fine

Props

AiCitationCheck props
PropTypeDefault
value

Controlled text.

string—
defaultValue

Initial text when uncontrolled.

string""
onValueChange

Called with the new text on every edit or fix.

(value: string) => void—
verify

Looks a reference up (Crossref for DOIs, CourtListener for cases, a HEAD request for links). Rejections show as Unverified.

(ref: Reference, signal: AbortSignal) => Promise<VerifyResult>—
sourceName

Named in "No record of this in …".

string"the lookup"
year

The current year, for "hasn't happened yet". Defaults to the device's year.

number—
onSubmit

Called on submit. May return a Promise. Anything made up or unverified needs a second press.

(value: string) => void | Promise<void>—
maxVisible

References listed before "Show N more". Default 5.

number5
className

Class for the outer wrapper.

string—
textareaClassName

Class for the textarea.

string—
messages

Override any visible string.

Partial<Omit<Messages, "status" | "kind">> & { status?: Partial<Messages["status"]>; kind?: Partial<Messages["kind"]> }—

* required. Also accepts the native props of <textarea> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Textarea gets aria-invalid and aria-describedby pointing at the summary
  • Lookup progress is announced in a role="status" region; the submit confirmation uses role="alert"
  • "Show" moves focus to the reference in the list
  • Statuses use text and icons, not colour alone; motion respects prefers-reduced-motion

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Without a verify function it only checks the text for tells; it cannot confirm anything exists
  • A reference that exists may still be misquoted or not support the claim
  • Publisher-prefix and reporter heuristics cover common cases, not every journal or court
  • Submitting is still possible after a second press

What it stops

Unverified references slip into AI text

Also known as

citations · references · bibliography · doi · crossref · courtlistener · legal brief · hallucination

Field type

Dependencies

Pairs well with