react-pdf-kit-custom-layout — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited react-pdf-kit-custom-layout (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use this skill when: the developer asks to replace the default viewer layout entirely with their own. Examples: embedded inline preview, a sidebar-only minimal reader, an enterprise UI with a custom toolbar on the side instead of the top. For small toolbar tweaks while keeping the default layout, use react-pdf-kit-toolbar-customization.
The viewer is composable. RPProvider and RPPages are the minimum required. Everything else (RPLayout, RPTheme, individual toolbar tools) can be swapped or omitted. State and actions are exposed via documented hooks.
RPProvider mountsthe document and makes the contexts available. RPPages renders the virtualized page list. Removing either breaks the viewer.
React.useContext(InternalContext) is unsupported and will break on minor releases. Documented hooks: useDocumentContext, useZoomContext, usePaginationContext, useSearchContext, useHighlightContext, useRotationContext, useViewModeContext, useDarkModeContext, plus feature hooks under utils/hooks/ re-exported from the public entry.
RPPages in anyancestor that doesn't have a measurable height (no flex: 1 / min-height: 0, no fixed height) makes the virtualizer mount with 0 rows. Always give it a real viewport.
RPConfig must be outermost, thenRPProvider, then RPTheme (optional but harmless) inside that. Anywhere inside RPProvider you can call the hooks.
provides CSS custom properties that built-in components (page layers, text selection highlight) read. Skipping it works visually but loses dark-mode and theming.
the default layout component, so neither appears in the final composition. Older code that imports RPDefaultLayout should migrate to RPLayout first (RPDefaultLayout is deprecated in v2) before headless migration, so the two refactors don't get tangled.
// src/HeadlessPdfViewer.tsx
import {
RPConfig,
RPProvider,
RPTheme,
RPPages,
} from '@react-pdf-kit/viewer'
import { CustomShell } from './CustomShell'
export function HeadlessPdfViewer({ src }: { src: string }) {
return (
<RPConfig>
<RPProvider src={src}>
<RPTheme>
<CustomShell />
</RPTheme>
</RPProvider>
</RPConfig>
)
}CustomShell (next step) is where YOU place RPPages next to your own toolbar, sidebar, or status bar.
// src/CustomShell.tsx
import {
RPPages,
useDocumentContext,
useZoomContext,
usePaginationContext,
useDarkModeContext,
} from '@react-pdf-kit/viewer'
export function CustomShell() {
const { numPages, isLoading } = useDocumentContext()
const { zoom, setZoom } = useZoomContext()
const { currentPage, goToPage } = usePaginationContext()
const { isDarkMode, toggleDarkMode } = useDarkModeContext()
return (
<div
style={{
display: 'grid',
gridTemplateColumns: '240px 1fr',
gridTemplateRows: '48px 1fr',
height: '100%',
}}
>
<header
style={{
gridColumn: '1 / -1',
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '0 16px',
borderBottom: '1px solid var(--rp-border, #e5e7eb)',
}}
>
<span>{isLoading ? 'Loading...' : `${numPages} pages`}</span>
<button
type="button"
onClick={() => setZoom(zoom + 0.1)}
aria-label="Zoom in"
>
+
</button>
<button
type="button"
onClick={() => setZoom(zoom - 0.1)}
aria-label="Zoom out"
>
-
</button>
<span style={{ flex: 1 }} />
<button
type="button"
onClick={toggleDarkMode}
aria-pressed={isDarkMode}
>
{isDarkMode ? 'Light' : 'Dark'}
</button>
</header>
<aside
style={{
overflow: 'auto',
borderRight: '1px solid var(--rp-border, #e5e7eb)',
padding: 8,
}}
>
{Array.from({ length: numPages }, (_, i) => i + 1).map(p => (
<button
key={p}
type="button"
onClick={() => goToPage(p)}
aria-current={p === currentPage ? 'page' : undefined}
style={{ display: 'block', width: '100%', textAlign: 'left' }}
>
Page {p}
</button>
))}
</aside>
<main style={{ overflow: 'hidden', minHeight: 0 }}>
<RPPages />
</main>
</div>
)
}The <main> ancestor of RPPages MUST have min-height: 0 (combined with the parent grid's 1fr row) so the virtualizer's parent has a measurable height.
If you compute derived state to pass into RPProvider (for example, a file-loading callback or options object), memoize it. The provider re-renders all children on identity changes, so a new object every render disables virtualization gains.
const options = useMemo(() => ({ withCredentials: true }), [])
return <RPProvider src={src} options={options}>...</RPProvider>Same pattern: import the hook, call it, render UI bound to its state and actions. useSearchContext, useHighlightContext, useRotationContext, useViewModeContext are all documented.
pnpm install
pnpm build
pnpm devOpen the page. Confirm:
currentPage (verify thearia-current indicator).
DOM (DevTools, Elements panel: RPPages should render a windowed subset, not all pages at once).
react-pdf-kit-setup: first-time setup.react-pdf-kit-toolbar-customization: for keeping the defaultlayout but tweaking toolbar contents.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.