PDF Viewer
A PDF in the browser’s own viewer with a toolbar around it. It delegates the rendering on purpose, and always shows a download link — because embedded PDFs fail routinely.
src/components/ui/pdf-viewer.tsxAbout
From the component's own source, where it was written.
A PDF, shown in the browser's own viewer with a toolbar around it.
**It delegates the rendering, on purpose.** Every browser ships a complete, sandboxed, accessible, keyboard-navigable PDF viewer with text selection, search and printing already built. The alternative is ~350 kB of pdf.js plus a worker, to arrive at a worse version of what is already installed. If you need what only a parser can give you — extracting text, drawing annotations, rendering a page to an image, a thumbnail rail — then reach for pdf.js directly; this component is honest that it is not that.
What it does own is the surface: a titled frame, page and zoom controls that drive the viewer through the PDF open parameters, and download and open actions that keep working when the embed does not.
**The fallback is the point of the design.** Embedded PDFs fail routinely — iOS Safari has historically rendered only the first page, some enterprise builds disable the plugin entirely, and a Content-Disposition of attachment on the response means it never displays. A viewer without a visible download link fails silently and looks broken; this one always shows the way out.
src is put in an iframe, so it must be same-origin or served with a permissive X-Frame-Options/frame-ancestors. A cross-origin PDF that refuses framing shows the fallback, which is the correct outcome rather than a blank rectangle.
Install
npx astralyx-ui add pdf-viewer
Usage
import { PdfViewer } from '@/components/ui/pdf-viewer'
<PdfViewer src="/invoice.pdf" title="Invoice 2026-04" pageCount={3} />Composer
Change the props and the source below follows.
Examples
A document with a toolbar
<PdfViewer src="/invoice.pdf" title="Invoice 2026-04" pageCount={3} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
PdfViewer
Extends Omit<ComponentProps<'div'>, 'title'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | required | Framed, so it must be same-origin or serve permissive frame-ancestors. A cross-origin PDF that refuses framing shows the fallback, which is the correct outcome. |
| title | ReactNode | — | — |
| size | string | — | Bytes, shown beside the title when known. |
| height | number | string | 520 | Viewer height. A PDF has no intrinsic height to grow into. |
| page | number | 1 | A hint, not a standard every viewer implements — Chrome and Firefox honour them, others ignore them. |
| pageCount | number | — | Total pages, when the caller knows it. Enables the page counter. |
| zoom | number | 100 | A hint, not a standard every viewer implements — Chrome and Firefox honour them, others ignore them. |
| toolbar | boolean | true | Hide the toolbar for a bare embed. |
| downloadName | string | — | Suggested filename for the download action. |
| onPageChange | (page: number) => void | — | — |
| label | string | 'PDF document' | — |
| emptyLabel | string | 'No document.' | — |
| fallback | ReactNode | — | iOS Safari has rendered only the first page, enterprise builds disable the plugin, and Content-Disposition: attachment means it never displays. A viewer without a download link fails silently and looks broken. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| delegates rendering | on purpose | Every browser ships a sandboxed, accessible, keyboard-navigable PDF viewer with search and printing. The alternative is ~350 kB of pdf.js plus a worker to arrive at a worse version of what is installed. |
| when to use pdf.js | stated | Extracting text, drawing annotations, rendering a page to an image, a thumbnail rail. This component is honest that it is not that. |
| vs FilePreview | the gap it fills | FilePreview dispatches images, audio, text and code, and gives PDFs an honest "no preview". This is the branch it was missing. |