import { FileViewer } from "@elabs-ai/components-viewer";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-viewerPaste 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
Pairs with
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
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Replace the default composition. Rendered inside the provider AND the frame. |
source | FileSource | The file to show. `undefined` is the empty state, not an error. |
registry | ViewerRegistry | Adapters available to this viewer. Defaults to the built-ins. Pass your own to add a format, drop one, or override a built-in. |
loading | boolean | Force 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. |
baseHeadingLevel | ProseHeadingLevel | The 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`. |
highlights | readonly 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. |
defaultHighlights | readonly DocumentHighlight[] | Uncontrolled initial citations. Ignored when `highlights` is supplied. |
onHighlightsChange | (highlights: readonly DocumentHighlight[]) => void | |
activeHighlightId | string | null | Which citation the viewer is pointed at. `null` is "none" explicitly; `undefined` is what selects uncontrolled mode, so the two are not interchangeable here. |
defaultActiveHighlightId | string | null | |
onActiveHighlightChange | (id: string | null) => void | |
pageNumber | number | Which 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. |
defaultPageNumber | number | Uncontrolled initial page. Ignored when `pageNumber` is supplied. |
onPageNumberChange | (page: number) => void | |
zoom | ZoomLevel | The 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. |
defaultZoom | ZoomLevel | Uncontrolled 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 | |
rotation | DocumentRotation | Quarter-turns clockwise — controlled. |
defaultRotation | DocumentRotation | Uncontrolled initial rotation. Ignored when `rotation` is supplied. Default `0`. |
onRotationChange | (rotation: DocumentRotation) => void |
FileViewerContent
FileViewerEmpty
FileViewerError
FileViewerFind
FileViewerFrame
FileViewerHighlightStatus
FileViewerPager
FileViewerProvider
| Prop | Type | Description |
|---|---|---|
source | FileSource | The file to show. `undefined` is the empty state, not an error. |
registry | ViewerRegistry | Adapters available to this viewer. Defaults to the built-ins. Pass your own to add a format, drop one, or override a built-in. |
loading | boolean | Force 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. |
baseHeadingLevel | ProseHeadingLevel | The 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`. |
highlights | readonly 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. |
defaultHighlights | readonly DocumentHighlight[] | Uncontrolled initial citations. Ignored when `highlights` is supplied. |
onHighlightsChange | (highlights: readonly DocumentHighlight[]) => void | |
activeHighlightId | string | null | Which citation the viewer is pointed at. `null` is "none" explicitly; `undefined` is what selects uncontrolled mode, so the two are not interchangeable here. |
defaultActiveHighlightId | string | null | |
onActiveHighlightChange | (id: string | null) => void | |
pageNumber | number | Which 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. |
defaultPageNumber | number | Uncontrolled initial page. Ignored when `pageNumber` is supplied. |
onPageNumberChange | (page: number) => void | |
zoom | ZoomLevel | The 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. |
defaultZoom | ZoomLevel | Uncontrolled 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 | |
rotation | DocumentRotation | Quarter-turns clockwise — controlled. |
defaultRotation | DocumentRotation | Uncontrolled initial rotation. Ignored when `rotation` is supplied. Default `0`. |
onRotationChange | (rotation: DocumentRotation) => void | |
childrenrequired | ReactNode |
FileViewerRotate
FileViewerSkeleton
FileViewerToolbar
| Prop | Type | Description |
|---|---|---|
actions | ReactNode | Extra controls, placed after the built-in actions. |
FileViewerZoom
Theming
The tokens each state resolves to. Change the token and every theme follows.
| State | Token |
|---|---|
| frame | bg-card + border-border + shadow-sm — a resting raised surface, not a float |
| toolbar divider | border-border-strong — the sole structural cue between toolbar and content (same fill, no elevation change), so the WCAG 1.4.11 strong rung |