Xzero UI
Pro library

Document extractionPro

Turns a document into typed data with a model and shows where every value came from: fields fill in as the page is read, and hovering a value lights its source on the PDF.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/document-extract
Live · keyboard ready

Pass a document, a schema and extract, which streams field values with their source boxes; onConfirm receives the checked data. With no props it extracts a sample invoice.

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/document-extract

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { DocumentExtract, documentToText, invoiceSchema, toJsonSchema } from "@/components/xzero/blocks/document-extract/document-extract";

export function InvoiceInbox({ document }) {
  return (
    <DocumentExtract
      document={document}
      schema={invoiceSchema}
      extract={async function* (doc, schema, { signal }) {
        // Your route sends documentToText(doc) and toJsonSchema(schema) to the model as structured output,
        // and streams back one line of JSON per value: { path, value, source, confidence, note }.
        const response = await fetch("/api/extract", {
          method: "POST",
          body: JSON.stringify({ text: documentToText(doc), schema: toJsonSchema(schema) }),
          signal,
        });
        const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
        let buffer = "";
        for (;;) {
          const { value, done } = await reader.read();
          if (done) break;
          buffer += value;
          const lines = buffer.split("\n");
          buffer = lines.pop() ?? "";
          for (const line of lines) if (line.trim()) yield { type: "value", ...JSON.parse(line) };
        }
      }}
      onAccept={data => saveInvoice(data)}
    />
  );
}

// Mark the spans a value can cite, so the form can point back at them.
const document = {
  id: "inv-0418",
  name: "larkspur-invoice-0418.pdf",
  label: "Invoice",
  kind: "pdf",
  blocks: [
    { type: "masthead", logo: { name: "larkspur", detail: "studio", color: "#3d4fc4", symbol: "petals" }, title: "Invoice" },
    { type: "meta", items: [["Invoice no.", [{ span: "number", text: "INV-2026-0418" }]], ["Issue date", [{ span: "issue", text: "14 October 2026" }]]] },
  ],
};

API reference

DocumentExtract

Turns a document into typed data with a model and shows where every value came from. The document sits in a quiet PDF viewer: a slim toolbar with the file name, page count and zoom, and the page as a white A4 sheet (white in dark mode too, like paper) on a slightly darker canvas. An email renders as a message view. The schema's fields sit beside it as an editable form in color groups. Extract streams values in while one soft scan bar passes down the page and each cited span takes a faint tint in its group's color. Hovering, focusing or clicking a field or a span lights both ends and draws one curved connector between them in that color, with a single sheen along it. Low confidence values carry the model's reason and a Looks right button; edits update the JSON tab live. Accept all confirms every value with a short check cascade and hands the typed output to onAccept.

PropTypeDefaultDescription
documentSourceDocument–The document to read: { id, name, label, kind?, meta?, blocks }. kind is "pdf" (an A4 sheet with a page count) or "email" (a message view), inferred from the file name when left out. Blocks are masthead (a text wordmark and title), email (subject, sender, recipient, date), heading, paragraph, lines (an address under a label), pairs, meta (a strip of label and value cells), columns, table (with optional detail lines), totals (the strong row sits on a double rule), rule and footer. Text is a string or an array of segments, where { span, text } marks a source span a value can cite. Without it the preview offers its two samples, an invoice and a shipping email.
schemaExtractSchemainvoiceSchemaThe fields to fill: { name, title, description?, groups?, fields }. A field is { key, label, type, description?, group? } with type string, date, money, currency, iban, integer or number; a table is { key, label, type: "table", group?, columns }. Each group in groups takes the next series color (--series-1 to --series-4) for its dot, rows, spans and connector; without groups each field takes its own. The Schema tab shows toJsonSchema(schema).
samples{ document: SourceDocument; schema: ExtractSchema }[]sampleDocumentsSeveral documents to switch between from a menu on the file name, each with its own schema. Ignored when document is set.
extract(document, schema, { signal }) => AsyncIterable<ExtractEvent> | Promise<ExtractEvent[]>–Your model call. Yield { type: "value", path, value, source?, confidence, note? } as values arrive, { type: "usage", inputTokens, outputTokens } for the cost readout, and { type: "error", message } or throw to show the failure state. Paths are field keys, or table.row.column for a cell such as lineItems.0.amount; source is the span id. Respect signal; Stop and Escape abort it. Without it the preview simulates the run and fails the first run of the shipping email so Retry can be tried.
onAccept(data: ExtractOutput, detail: { document, schema, values }) => void | Promise<void>–Called by Accept all after every value turns accepted. data has one key per schema field, typed: dates as YYYY-MM-DD strings, money and numbers as numbers, IBANs without spaces, tables as arrays of row objects. Accept all first asks to fix any value that does not fit its type. Throw to undo the accept. Without it the preview simulates the hand-off and says so.
modelExtractModel{ name: "Claude Haiku 5.5", id: "claude-haiku-5-5", inputPrice: 0.1, outputPrice: 0.5 }The model named in the footer. Prices are US dollars per million tokens and drive the cost readout; id is named in the Schema tab.
modelMarkReactNodeClaude markThe model's mark in the footer.
reviewThresholdnumber0.8Values with a confidence below this are flagged with a warning, show their reason, and count toward the to check summary.
classNamestring–Class on the root element.

Keyboard interactions

KeysAction
TabMoves through the file menu, the zoom controls and Fit, the page canvas, the output tabs, Extract, each field and table cell, every Looks right button, Review and Accept all.
Focus a fieldSelects it: the page scrolls to its source span, both ends light in the group's color and the connector draws between them. Tabbing through the form walks the document alongside.
Control or Command with = / - / 0With the page canvas focused, zooms in, zooms out, or fits the page to the width.
Arrow keysScroll the focused page canvas and the JSON blocks; on the output tabs and in the file menu they move the selection.
EscapeStops a running extraction. Otherwise clears the selected field.
Enter, SpaceActivates Extract, Stop, Retry, Review, Looks right, the zoom controls, Fit, the copy buttons and Accept all, and picks a document in the file menu.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Guidance for AI tools

Blocks are complete, self-contained screens with sample data. Replace the sample data and connect the callbacks described above. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components