Skip to content

PDF

PDF is exported from a subpath (not the main barrel) so it isn’t bundled into apps that don’t use it:

import PDF from '@delightstack/components/pdf';
import type { PDFAnnotation } from '@delightstack/components';

PDF requires pdfjs-dist as a peer dependency (loaded lazily at runtime):

Terminal window
pnpm add pdfjs-dist

The PDF worker is loaded automatically from unpkg at runtime.

Currently viewing page 1.

View code
<script>
import PDF from '@delightstack/components/pdf';
let page = $state(1);
</script>
<PDF src="/documents/report.pdf" bind:page height="500px" />

Bind the zoom level for external control.

<script>
import PDF from '@delightstack/components/pdf';
let zoom = $state(1);
</script>
<button onclick={() => zoom = 1.5}>150%</button>
<button onclick={() => zoom = 1}>100%</button>
<PDF src="/documents/report.pdf" bind:zoom />

Load a PDF from an ArrayBuffer or Uint8Array (e.g. from a file input).

<script>
import PDF from '@delightstack/components/pdf';
let pdfData = $state<ArrayBuffer | undefined>();
async function handleFile(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) pdfData = await file.arrayBuffer();
}
</script>
<input type="file" accept=".pdf" onchange={handleFile} />
{#if pdfData}
<PDF src={pdfData} />
{/if}

Enable annotation tools (highlight and note modes) in the toolbar.

<PDF
src="/documents/contract.pdf"
annotatable
onannotation={(annotation) => {
console.log(annotation.type, 'on page', annotation.page, annotation.data);
}}
/>

Hide the toolbar and download button for an embedded read-only view.

View code
<PDF src="/documents/preview.pdf" show_toolbar={false} height="400px" />

single_page renders only the current page (no internal scroll, no page-stack), and lets an external page navigator drive page. This is the mode the Carousel uses when displaying PDF items, but you can use it directly any time you want to wire your own page controls.

Page 1

single_page renders only the current page, disables internal scroll, and fits the page to the container — designed for embedding inside Gallery/Carousel or any external page navigator.

View code
<script>
import { Button } from '@delightstack/components';
import PDF from '@delightstack/components/pdf';
let page = $state(1);
let total_pages = $state(0);
</script>
<PDF
src="/documents/preview.pdf"
bind:page
single_page
show_toolbar={false}
fit="page"
onload={(d) => total_pages = d.total_pages} />
<Button onclick={() => page--} disabled={page <= 1}>‹ Prev page</Button>
<span>Page {page} / {total_pages}</span>
<Button onclick={() => page++} disabled={page >= total_pages}>Next page ›</Button>
View code
<PDF src={url_when_loaded} height="600px" />
PropTypeDefaultDescription
srcstring | ArrayBuffer | Uint8Array-PDF source: URL string, ArrayBuffer, or Uint8Array. May be assigned later — the skeleton shows until it arrives
pagenumber1Current page number, 1-based ($bindable)
zoomnumber1Zoom level where 1 = 100% ($bindable)
rotationnumber0Rotation in degrees (0, 90, 180, 270)
fit'width' | 'height' | 'page''width'Initial fit mode
show_toolbarbooleantrueShow the toolbar
show_downloadbooleantrueShow the download button in the toolbar
searchablebooleantrueEnable text search
annotatablebooleanfalseEnable annotation tools
heightstring'600px'Container height
single_pagebooleanfalseRender only the current page, disable internal scroll, and fit the page to the container. Designed for embedding inside Gallery/Carousel or any external page navigator.
skeletonbooleantrueBuilt-in loading state: shows a skeleton while the document loads (including before src is available) and dismisses itself when ready. Set false to disable
text_layerbooleantrueRender the selectable/searchable text layer over each page. Disable it (the Carousel does this automatically) to skip text extraction and per-glyph layout for faster rendering when selection isn’t needed. Search still works.
idstringautoElement ID
classstring''Additional CSS classes
elementHTMLElementundefinedBindable reference to the root element ($bindable)
EventDetailDescription
onpagechange{ page: number, total_pages: number }Fires when the current page changes
onload{ total_pages: number }Fires when the PDF finishes loading
onerror{ error: Error }Fires when the PDF fails to load
ondownloadnoneFires when the download button is clicked
onannotationPDFAnnotationFires when an annotation is created
interface PDFAnnotation {
type: 'highlight' | 'note';
page: number;
data: unknown;
}
  • Container has role="document" with aria-label="PDF viewer"
  • tabindex="0" for keyboard focus
  • Ctrl/Cmd + F opens text search
  • Ctrl/Cmd + +/- zooms in/out, Ctrl/Cmd + 0 resets zoom
  • Arrow Left / Page Up goes to the previous page
  • Arrow Right / Page Down goes to the next page
  • Home / End jumps to the first / last page
  • Search input supports Enter for next match, Shift+Enter for previous match
  • Toolbar buttons have descriptive aria-label attributes
  • Page input has aria-label="Page number"
  • Text is selectable for highlighting annotations
  • Focus-visible outlines on all interactive elements