Skip to main content

Recipes

Viewerdisplay

Attachments, uploads, side-by-side revisions and a custom format

Render a file the app did not write (upload, signed URL, agent output) — detects the format, loads the matching adapter on demand, draws it with brand-ui components.

import { FileViewer } from "@elabs-ai/components-viewer";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-viewer

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui FileViewer component (from @elabs-ai/components-viewer) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look FileViewer up before writing code: `npx -y @elabs-ai/components-cli docs FileViewer` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-viewer is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-viewer` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-viewer`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Avoid

  • Adding a per-format branch to FileViewerContent — a format is an adapter registration (registry.register), never a switch in the shell.
  • Making a parser a plain dependency — every parser engine is an OPTIONAL peer, so a consumer who skips it gets the panel naming the package instead of a build error.
  • Returning HTML from an adapter — adapters emit a DATA model that brand-ui components render; that is the only reason an arbitrary file inherits the theme.
  • Using FileViewer to edit — it is read-only; authoring code is CodeEditor (@elabs-ai/components-editor).
  • Clearing an error with the `loading` prop — a parent's loading is additive; it can add the not-ready state but never hide a real failure.

Works with

Contains

  • FileViewerToolbar
  • FileViewerContent

Pairs with

  • FileViewerProvider
  • FileViewerSkeleton
  • FileViewerError
  • FileViewerEmpty

Examples

4 live examples

Upload And Preview

`{ kind: "file", file }` takes a `File` straight from an input or a drop: nothing is uploaded and nothing leaves the browser. Pick any file on your machine to try the adapters on real content; with nothing picked the viewer shows its empty state.

Compare Revisions

Two providers, two documents, one screen. Each side marks the passages that differ from the other through the same highlight layer citations use, so "what changed" is a list of quotes — the output of any text diff — and not a second rendering mode.

Custom Format

The registry is the extension point: this story adds a subtitle (`.srt`) adapter in forty lines and hands the registry to the viewer. The shell — identity row, download, loading, empty and error states — is the same one every built-in format gets, because the adapter supplies only `load` and a `Renderer`.

API

FileViewer

PropTypeDescription
childrenReactNodeReplace the default composition. Rendered inside the provider AND the frame.
sourceFileSourceThe file to show. `undefined` is the empty state, not an error.
registryViewerRegistryAdapters available to this viewer. Defaults to the built-ins. Pass your own to add a format, drop one, or override a built-in.
loadingbooleanForce the not-ready state while a parent fetches the source itself. ORed with the viewer's own loading — a parent can add loading, never remove it.
baseHeadingLevelProseHeadingLevelThe rung a viewed document's own top-level heading renders at. Default `2`. A file carries its OWN heading tree, and that tree is only correct relative to the page hosting it: a README's `#` rendered as an `<h1>` inside an app that already has one puts two `h1`s in a screen reader's flat heading list, and the frame's `<section aria-label>` does not fix that — most screen readers list headings flat, not per landmark. The default assumes the common case, a viewer embedded BELOW the page's own heading; pass `1` when the viewer genuinely is the page. Adapters that render no headings ignore it. Same seam as `@elabs-ai/components-ai`'s `MarkdownView baseHeadingLevel`.
highlightsreadonly DocumentHighlight[]The parts of the document to point at — an answer's citations, a search result's context, anything the app already knows about the file. A PROP rather than provider-only state because citations originate outside the viewer entirely: the chat pane that produced them usually lives in another route, and it owns which one the reader clicked. Pass `defaultHighlights` instead to let the viewer own them.
defaultHighlightsreadonly DocumentHighlight[]Uncontrolled initial citations. Ignored when `highlights` is supplied.
onHighlightsChange(highlights: readonly DocumentHighlight[]) => void
activeHighlightIdstring | nullWhich citation the viewer is pointed at. `null` is "none" explicitly; `undefined` is what selects uncontrolled mode, so the two are not interchangeable here.
defaultActiveHighlightIdstring | null
onActiveHighlightChange(id: string | null) => void
pageNumbernumberWhich page the viewer is on, 1-based — controlled. A trio (`component-api.md`) rather than provider-only state because the page is routinely something the APP owns: a deep link to page 7, a URL the reader can share, a position restored from a "continue reading" record. Clamped to the document on read, so an out-of-range value degrades to the nearest real page instead of blanking the canvas.
defaultPageNumbernumberUncontrolled initial page. Ignored when `pageNumber` is supplied.
onPageNumberChange(page: number) => void
zoomZoomLevelThe scale to draw at, or a fit mode — controlled. Persisting a reader's preferred zoom across files and sessions is the reason this is a prop.
defaultZoomZoomLevelUncontrolled initial zoom. Ignored when `zoom` is supplied. Defaults to `"fit-width"`, not `1`: a viewer's job on open is to show the document, and a 4000px scan or an A4 page at 100% in a 600px pane shows its top-left corner. Every reader-facing PDF viewer opens fitted for the same reason. Pass `1` for true 100%.
onZoomChange(zoom: ZoomLevel) => void
rotationDocumentRotationQuarter-turns clockwise — controlled.
defaultRotationDocumentRotationUncontrolled initial rotation. Ignored when `rotation` is supplied. Default `0`.
onRotationChange(rotation: DocumentRotation) => void

Also accepts: Omit<FileViewerProviderProps, "children">, Omit<HTMLAttributes<HTMLDivElement>, "children">

FileViewerContent

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerEmpty

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerError

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerFind

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerFrame

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerHighlightStatus

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerPager

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerProvider

PropTypeDescription
sourceFileSourceThe file to show. `undefined` is the empty state, not an error.
registryViewerRegistryAdapters available to this viewer. Defaults to the built-ins. Pass your own to add a format, drop one, or override a built-in.
loadingbooleanForce the not-ready state while a parent fetches the source itself. ORed with the viewer's own loading — a parent can add loading, never remove it.
baseHeadingLevelProseHeadingLevelThe rung a viewed document's own top-level heading renders at. Default `2`. A file carries its OWN heading tree, and that tree is only correct relative to the page hosting it: a README's `#` rendered as an `<h1>` inside an app that already has one puts two `h1`s in a screen reader's flat heading list, and the frame's `<section aria-label>` does not fix that — most screen readers list headings flat, not per landmark. The default assumes the common case, a viewer embedded BELOW the page's own heading; pass `1` when the viewer genuinely is the page. Adapters that render no headings ignore it. Same seam as `@elabs-ai/components-ai`'s `MarkdownView baseHeadingLevel`.
highlightsreadonly DocumentHighlight[]The parts of the document to point at — an answer's citations, a search result's context, anything the app already knows about the file. A PROP rather than provider-only state because citations originate outside the viewer entirely: the chat pane that produced them usually lives in another route, and it owns which one the reader clicked. Pass `defaultHighlights` instead to let the viewer own them.
defaultHighlightsreadonly DocumentHighlight[]Uncontrolled initial citations. Ignored when `highlights` is supplied.
onHighlightsChange(highlights: readonly DocumentHighlight[]) => void
activeHighlightIdstring | nullWhich citation the viewer is pointed at. `null` is "none" explicitly; `undefined` is what selects uncontrolled mode, so the two are not interchangeable here.
defaultActiveHighlightIdstring | null
onActiveHighlightChange(id: string | null) => void
pageNumbernumberWhich page the viewer is on, 1-based — controlled. A trio (`component-api.md`) rather than provider-only state because the page is routinely something the APP owns: a deep link to page 7, a URL the reader can share, a position restored from a "continue reading" record. Clamped to the document on read, so an out-of-range value degrades to the nearest real page instead of blanking the canvas.
defaultPageNumbernumberUncontrolled initial page. Ignored when `pageNumber` is supplied.
onPageNumberChange(page: number) => void
zoomZoomLevelThe scale to draw at, or a fit mode — controlled. Persisting a reader's preferred zoom across files and sessions is the reason this is a prop.
defaultZoomZoomLevelUncontrolled initial zoom. Ignored when `zoom` is supplied. Defaults to `"fit-width"`, not `1`: a viewer's job on open is to show the document, and a 4000px scan or an A4 page at 100% in a 600px pane shows its top-left corner. Every reader-facing PDF viewer opens fitted for the same reason. Pass `1` for true 100%.
onZoomChange(zoom: ZoomLevel) => void
rotationDocumentRotationQuarter-turns clockwise — controlled.
defaultRotationDocumentRotationUncontrolled initial rotation. Ignored when `rotation` is supplied. Default `0`.
onRotationChange(rotation: DocumentRotation) => void
childrenrequiredReactNode

FileViewerRotate

Also accepts: HTMLAttributes<HTMLButtonElement>

FileViewerSkeleton

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerToolbar

PropTypeDescription
actionsReactNodeExtra controls, placed after the built-in actions.

Also accepts: HTMLAttributes<HTMLDivElement>

FileViewerZoom

Also accepts: HTMLAttributes<HTMLDivElement>

Theming

The tokens each state resolves to. Change the token and every theme follows.

StateToken
framebg-card + border-border + shadow-sm — a resting raised surface, not a float
toolbar dividerborder-border-strong — the sole structural cue between toolbar and content (same fill, no elevation change), so the WCAG 1.4.11 strong rung