# Document viewer

A React component in [Beamline](https://beamline.io/)'s [Content & display](https://beamline.io/components/display) category. Live demo: https://beamline.io/components/document-viewer

Part of [Beamline UI: React UI components and complete screens for apps built with coding agents](https://beamline.io/). [The code is copied into your repo; your app never depends on our servers](https://beamline.io/docs/install). [Build your own style from any look; tokens, fonts, radii, motion; not only colours](https://beamline.io/docs/customize). [200+ components and 50+ complete screens](https://beamline.io/components/). [$49 launch price; one payment; your whole team, no seats. Lifetime updates: every new version of the complete system, for as long as we publish it, and every version you received stays yours](https://beamline.io/pricing). [React 19, TypeScript, Tailwind v4, Radix](https://beamline.io/docs/install). [Free MIT sample, commercial use included](https://github.com/dprvda/beamline). [Made by Danylo Pravda in Kyiv, Ukraine](https://beamline.io/about).

A PDF or a paged document read and marked up in place: page thumbnails beside it, zoom to the width, the page or a percentage, pages by keyboard, a search that marks every hit and steps through them, and annotations (highlight text or mark an area, attach a comment) listed beside the pages, each jumping to its place; pages not drawn yet are hatched, and loading, empty, failed and password-locked documents each say so. pdf.js loads only when a PDF is opened.

## Use it for

- Reading a PDF inside the product: a contract, an invoice, a statement, a report, a signed form.
- Review screens where people mark passages or areas of a document and comment on them.
- Scanned or pre-rendered pages (images) that read as one paged document.

## Not for

- One image or a set of pictures at full size ([lightbox](https://beamline.io/components/lightbox)).
- Writing or editing the document's text ([rich-text-editor](https://beamline.io/components/rich-text-editor)).
- Comparing two versions of plain text or code line by line ([file-diff](https://beamline.io/components/file-diff)).
- A file's tile in a message or a form ([attachment](https://beamline.io/components/attachment)).

## Anatomy

- toolbar: thumbnails toggle; page field (current of total) with previous and next; zoom out, the zoom menu (Fit width, Fit page, 50–400 %) and zoom in; search with hit count and previous / next; Mark area; annotations toggle with their count; your actions
- thumbnails rail: one small page per page with its number, a plate that travels to the current page, a mark on pages with annotations and on pages with search hits
- pages: each page drawn at the zoom with its selectable text, the hits of the search (the current one outlined), the annotations (a tinted highlight or a framed area, a numbered pin in the margin), and a hatched placeholder until it is drawn
- selection bar over selected text: Highlight and Comment
- annotations panel: every annotation in page order (pin number, its glyph and kind, page, the passage or area, the comment, who and when), an editor for a new comment, Delete for each
- states over the pages: loading (skeleton pages and how much has arrived), empty, failed (the reason and Try again), locked (a password field and Open)

## Variants

- **0**: source: a PDF (src: URL, bytes or a Blob) or image pages (pages)
- **1**: zoom: fit-width · fit-page · a percentage
- **2**: thumbnails rail on or off
- **3**: annotations panel on, off, or left out (annotationsPanel false: your own list drives selectedAnnotation)
- **4**: annotate: creating annotations on or off (read-only)
- **5**: height: px or fill

## States

- loading (skeleton pages, arrived share when the size is known)
- empty (no document, in your words)
- failed (could not open: the reason and Try again)
- locked (asks for the password; a wrong one says so in the field)
- page not drawn yet (hatched, numbered)
- searching (hits marked, the current one outlined, pages with hits marked in the rail)
- no hits
- selecting text (the selection bar)
- marking an area (crosshair; drag on a page)
- annotation selected (outlined in the page and in the list)
- writing a comment
- narrow room (phone): rail and panel open as sheets, toolbar folds

## Keyboard

- In the pages: ← → and Page Up / Page Down go to the previous and next page, Home and End to the first and last; ↑ ↓ scroll
- + and − zoom in and out, 0 fits the width, 9 fits the page
- / or ⌘F / Ctrl+F focuses the search; Enter and Shift+Enter step to the next and previous hit; Escape clears it
- The page field takes a number and Enter
- Thumbnails: ↑ ↓ move between pages, Enter opens one
- With a hit current, Mark highlights it: an annotation made from the keyboard
- Annotations list: ↑ ↓ between items, Enter jumps to the annotation in the page; Delete removes your own
- ⌘Enter saves a comment; Escape cancels it or leaves area marking

## Motion

A drawn page crossfades over its hatched placeholder (--pui-fade-in); the rail's plate glides to the current page (useIndicator, critically damped). Jumping to a page, a hit or an annotation glides the scroll (instant under reduced motion) and the target lights once: a hit's outline settles in, an annotation's tint flashes in and fades over 600 ms. A new highlight draws in along its lines (a wipe from the start of the passage); a new area's frame draws in from its corner. The zoom readout rolls; pages are resized at once and redrawn sharp in the background, so nothing waits on a frame loop. List items grow in and leave with a fading ghost (useFlip); the panel and rail fade in when opened. Nothing runs at rest: drawing happens only for pages that come into view, and no animation loops.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src` | `string \| URL \| ArrayBuffer \| Uint8Array \| Blob \| null` | — | The PDF: an address, its bytes or a Blob. |
| `pages` | `DocumentImagePage[]` | — | Image pages instead of a PDF. |
| `title` | `string` | — | The document's name. |
| `page / defaultPage / onPageChange` | `number` | — | The page being read, from 1. Setting it scrolls there. |
| `zoom / defaultZoom / onZoomChange` | `"fit-width" \| "fit-page" \| number` | — | Fit the width (default), the whole page, or a scale (1 = 100 %, 0.25–4). |
| `query / defaultQuery / onQueryChange` | `string` | — | The search: case and spacing are ignored; every hit is marked. |
| `annotations / defaultAnnotations / onAnnotationsChange` | `DocumentAnnotation[] / (list, event) => void` | — | { id, page, kind: "text" \| "area", rects?, quote?, comment?, author?, createdAt?, tone?, resolved?, icon? }. rects are fractions of the page from its top left; a text annotation with only a quote is found on its page. The event says what happened: create (with intent highlight or comment), comment, delete. |
| `selectedAnnotation / defaultSelectedAnnotation / onSelectedAnnotationChange` | `string \| null` | — | The annotation in focus. Setting it from outside jumps to it. |
| `annotate` | `boolean` | — | Offer Highlight, Comment and Mark area. Default true. |
| `author` | `{ name: string; src?: string }` | — | Who new annotations belong to. Default { name: "You" }. |
| `thumbnails / defaultThumbnails / onThumbnailsChange` | `boolean` | — | The thumbnails rail. Default shown when the room is 40rem or wider; on a phone the page number opens it as a sheet. |
| `panel / defaultPanel / onPanelChange` | `boolean` | — | The annotations panel. Default shown when there are annotations and the room is 56rem or wider. |
| `annotationsPanel` | `boolean` | — | false leaves the built-in annotations list out. Default true. |
| `actions` | `ReactNode` | — | Your controls at the toolbar's end. |
| `height` | `number \| "fill"` | — | Default 640; "fill" takes its container's height. |
| `loading` | `boolean` | — | Skeleton pages while your app is fetching the file. |
| `emptyLabel` | `string` | — | Default "No document to show". |
| `workerSrc` | `string` | — | Where your app serves pdf.js's worker. |
| `onLoad / onError` | `({ pages }) => void / (error: Error) => void` | — | The document opened, or could not be opened. |
| `aria-label` | `string` | — | Default: the title, or "Document". |
| `className / classNames / style / ref` | `string / DocumentViewerClassNames / CSSProperties / Ref<HTMLDivElement>` | — | className: Classes on the documented visual root; no override by default. classNames: Classes for the named visual parts; no overrides by default. style: Inline styles on the documented visual root; no overrides by default. ref: Receives the documented native target or imperative handle; omitted by default. |
| `page` | `number` | — |  |
| `defaultPage` | `number` | — |  |
| `onPageChange` | `((page: number) => void)` | — |  |
| `zoom` | `DocumentZoom` | — |  |
| `defaultZoom` | `DocumentZoom` | — |  |
| `onZoomChange` | `((zoom: DocumentZoom) => void)` | — |  |
| `query` | `string` | — |  |
| `defaultQuery` | `string` | — |  |
| `onQueryChange` | `((query: string) => void)` | — |  |
| `annotations` | `DocumentAnnotation[]` | — |  |
| `defaultAnnotations` | `DocumentAnnotation[]` | — |  |
| `onAnnotationsChange` | `((annotations: DocumentAnnotation[], event: DocumentAnnotationEvent) => void)` | — |  |
| `selectedAnnotation` | `string \| null` | — |  |
| `defaultSelectedAnnotation` | `string \| null` | — |  |
| `onSelectedAnnotationChange` | `((id: string \| null) => void)` | — |  |
| `thumbnails` | `boolean` | — |  |
| `defaultThumbnails` | `boolean` | — |  |
| `onThumbnailsChange` | `((shown: boolean) => void)` | — |  |
| `panel` | `boolean` | — |  |
| `defaultPanel` | `boolean` | — |  |
| `onPanelChange` | `((shown: boolean) => void)` | — |  |
| `onLoad` | `((info: { pages: number; }) => void)` | — |  |
| `onError` | `((error: Error) => void)` | — |  |
| `className` | `string` | — | Classes on the documented visual root; no override by default. |
| `classNames` | `Partial<Record<"root" \| "toolbar" \| "rail" \| "pages" \| "page" \| "panel", string>>` | — | Classes for the named visual parts; no overrides by default. |
| `style` | `CSSProperties` | — | Inline styles on the documented visual root; no overrides by default. |
| `ref` | `Ref<HTMLDivElement>` | — | Receives the documented native target or imperative handle; omitted by default. |

## Dependencies

`pdfjs-dist`, `lucide-react`

## Import

```tsx
import { DocumentViewer } from "@beamline/document-viewer";
```

## Get it

Document viewer is part of Beamline: 200+ components and 50+ complete screens for React, installed as local code in your repo. Your app never depends on Beamline servers at runtime. Build your own style from any look with public tokens, your fonts, radii, density and motion; custom style files stay in your app. Use local kit tools or let your coding agent find and install parts through the optional hosted MCP server. $49 launch price, one payment, no subscription, lifetime updates, one licence for your whole team. [Get Beamline](https://beamline.io/checkout?pack=complete) · [Connect your agent](https://beamline.io/connect)

## More in Content & display

- [Image compare](https://beamline.io/components/image-compare): Two images in one frame with a divider: drag anywhere, or arrow keys on the handle; horizontal or vertical, with captions.
- [Live cursors](https://beamline.io/components/live-cursors): Who else is working on this, and where: over any shared surface, each person's pointer in the colour their name always gets, with a name flag, gliding…
- [Video player](https://beamline.io/components/video): A video player on the native <video> element: title bar with an external link and close, centre play with ±10 s, scrub, time, volume, speed, captions…
- [Lightbox](https://beamline.io/components/lightbox): A full-screen image viewer: a thumbnail grows into it and shrinks back, arrows and ←/→ step through the set, Esc or a downward drag closes it.
- [File diff](https://beamline.io/components/file-diff): A change to one file as a unified diff: line numbers, tinted lines, only the changed words marked, long unchanged stretches folded; give rows or just…
- [QR Code](https://beamline.io/components/qr-code): A scannable code set as gradient dots with rounded finder rings and an optional centre mark; a new value ripples out from the centre.
- [Slot text](https://beamline.io/components/slot-text): Text and numbers that roll into their new value on reels, digit by digit from the right; other characters fade. Read as plain text by assistive tech.
- [Text reveal](https://beamline.io/components/text-reveal): A headline whose words rise into place once, the first time it scrolls into view; never repeats.
- [Text morph](https://beamline.io/components/text-morph): A short label that becomes its next state letter by letter: shared letters glide into place, new ones arrive, old ones leave.
- [Code block](https://beamline.io/components/code-block): Code to read and copy: highlighted in the system's colours, numbered, marked lines, long files folded behind “Show all”.
- [Inbox list](https://beamline.io/components/inbox-list): Conversations waiting for a person, triaged in place: sender, subject and the first words, time, unread weight, star, labels and attachments, grouped by…
- [Comment thread](https://beamline.io/components/comment-thread): A discussion attached to something: replies, reactions, @mentions, editing and deleting your own, resolve that folds the thread.
- [Timeline](https://beamline.io/components/timeline): What happened, newest first and grouped by day: people with avatars, systems with toned icons, relative times with the exact time on hover, details in…
- [Description list](https://beamline.io/components/description-list): The details of one record as label and value pairs: beside each other or stacked, in as many columns as the room holds; a copy button arrives on the row…
- [Text shimmer](https://beamline.io/components/text-shimmer): Ongoing work said in words (“Thinking…”) with a calm band of light passing across them; stops when inactive.
- [Logo Carousel](https://beamline.io/components/logo-carousel): A wall of logos shown a few at a time: every few seconds the slots turn left to right, each logo leaving upward as the next rises in.
- [Avatar group](https://beamline.io/components/avatar-group): A team in a small space: overlapping faces that name themselves on hover or focus, and “+N” naming the rest.
- [Avatar](https://beamline.io/components/avatar): A person or account in a small space: a photo, or initials on the hue their name always gets; presence dot; circle for people, square for companies.
- [Item](https://beamline.io/components/item): The generic list row: media, title, description, meta and trailing actions; becomes a link or button when interactive.
- [Marker](https://beamline.io/components/marker): A labelled point in a stream: a status event with an icon between messages, or a label such as "Today" between two hairlines.
- [Credit](https://beamline.io/components/credit): "Designed with Beamline" for a site's footer: the Beamline mark and a quiet line, or a small badge. With your referral code, a team's first purchase…
- [Badge](https://beamline.io/components/badge): A short status, category or count label: six tones, an icon or a status dot, two sizes.
- [Kbd](https://beamline.io/components/kbd): Keyboard keys as keycaps, for combinations and sequences (G then A); `mod` resolves to ⌘ or Ctrl per platform, symbols are spoken as words.
