Adjust Space Between Pages
Scenario
Section titled “Scenario”You want to adjust space between pages in the React PDF Viewer instead of displaying default spaces between pages.
- Pages look cramped and you want a visible gap between them for reading or screenshots.
- You want viewers to change that gap while the PDF is open (for example a slider or number field).
- Your layout should work when the viewer only keeps some pages in the DOM at once, not the full document at every scroll position.
- You want no extra gap on a single-page file, or when the chosen gap is zero.
What to Use
Section titled “What to Use”You can control the gap between pages and update it while the PDF stays open, while still using React PDF Kit’s page list and scroll behavior.
First, the document need to be loaded and ready. useDocumentContext tells you when the document is ready and how many pages it has.
| Name | Objective |
|---|---|
pdf | The loaded PDF document. This example reads pdf.numPages to size the gaps. |
loading | Whether the document is still loading. Wait until this is false before changing the page list. |
Once the document is ready, pages can be accessed via the data-rp attribute from React PDF Kit’s viewer element markup.
| Name | Objective |
|---|---|
pages | The wrapper element that contains the PDF page list. This is the first element we query before applying spacing. |
After the pages are accessed, you will need to use the following browser APIs to manage the gap between pages when the DOM changes. The React PDF Viewer component only mounts some page nodes at a time.
| Name | Objective |
|---|---|
MutationObserver | Run spacing again when pages are added or removed from the list. |
requestAnimationFrame | Poll each frame until [data-rp="pages"] exists after load (up to about 120 frames). |
cancelAnimationFrame | Stop polling when spacing is set up or the component unmounts. |
createElement | Create the <style> element when gap CSS is applied for the first time. |
getElementById | Find the style tag to update its CSS or remove it when spacing is cleared. |
appendChild | Attach the style tag to head so the gap rules take effect. |
addEventListener | Update spacing when scroll shows a different set of page rows. |
Code example
Section titled “Code example”-
Spacing utilities
This module provides the default gap value and the functions that locate the pages list and enable spacing.
src/pdfPageSpacing.js export const PAGE_GAP_PX = 24;const STYLE_TAG_ID = "pdf-page-gap-styles";const SPACING_CLASS_NAME = "pdf-pages-spaced";const PAGE_SELECTOR = '[data-rp="pages"]';function isValidSpacingInput(totalPages, gap) {return totalPages > 1 && gap > 0;}// Build CSS that adds visible space between PDF pages in the virtual list.//// Margins on page nodes do not work reliably here. Instead:// 1. box shadow below each page shows the gap (viewer background color).// 2. translateY on page 2 and later moves each page down by one more gap.function buildPageGapStyles(totalPages, gap) {if (!isValidSpacingInput(totalPages, gap)) return "";const rules = [`.pdf-pages-spaced [id^="page-"] {box-shadow: 0 ${gap}px 0 0 var(--rp-pages-background-color, #d8d8d8) !important;}`,];// Page 1 stays at y=0; each later page shifts by one more gap.for (let page = 2; page <= totalPages; page++) {const offset = (page - 1) * gap;rules.push(`.pdf-pages-spaced [data-rp="page-${page}"] { transform: translateY(${offset}px) !important; }`,);}return rules.join("\n");}function getOrCreateStyleTag() {let styleEl = document.getElementById(STYLE_TAG_ID);if (!styleEl) {styleEl = document.createElement("style");styleEl.id = STYLE_TAG_ID;document.head.appendChild(styleEl);}return styleEl;}function injectPageGapStyles(totalPages, gap) {const styleEl = getOrCreateStyleTag();styleEl.textContent = buildPageGapStyles(totalPages, gap);}function removePageGapStyles() {document.getElementById(STYLE_TAG_ID)?.remove();}function findGridInner(pagesContainer) {let tallestElement = null;let tallestHeight = 0;for (const el of Array.from(pagesContainer.querySelectorAll("div"))) {if (el.style.position !== "relative" || !el.style.height) continue;const height = Number.parseFloat(el.style.height);if (height > tallestHeight) {tallestHeight = height;tallestElement = el;}}return tallestElement;}function adjustInnerScrollHeight(pagesContainer, gap, totalPages) {if (!isValidSpacingInput(totalPages, gap)) return;const inner = findGridInner(pagesContainer);if (!inner) return;if (inner.dataset.originalHeight === undefined && inner.style.height) {inner.dataset.originalHeight = inner.style.height;}const originalHeight = Number.parseFloat(inner.dataset.originalHeight ?? "");if (Number.isNaN(originalHeight)) return;const targetHeight = originalHeight + (totalPages - 1) * gap;if (inner.style.height !== `${targetHeight}px`) {inner.style.height = `${targetHeight}px`;}}function removeSpacingFromContainer(pagesContainer) {pagesContainer.classList.remove(SPACING_CLASS_NAME);pagesContainer.style.removeProperty("--pdf-page-gap");removePageGapStyles();const inner = findGridInner(pagesContainer);if (inner?.dataset.originalHeight) {inner.style.height = inner.dataset.originalHeight;}}function applySpacingToContainer(pagesContainer, gap, totalPages) {if (gap <= 0 || totalPages <= 1) {removeSpacingFromContainer(pagesContainer);return;}pagesContainer.style.setProperty("--pdf-page-gap", `${gap}px`);pagesContainer.classList.add(SPACING_CLASS_NAME);injectPageGapStyles(totalPages, gap);adjustInnerScrollHeight(pagesContainer, gap, totalPages);}function findScrollTarget(pagesContainer) {return (pagesContainer.querySelector('[style*="overflow"]') ??pagesContainer.parentElement);}export function findPagesContainer(root) {return root.querySelector(PAGE_SELECTOR);}export function enablePageSpacing(pagesContainer, gap, getTotalPages) {const updateSpacing = () => {const totalPages = getTotalPages();if (totalPages <= 0) return;applySpacingToContainer(pagesContainer, gap, totalPages);};updateSpacing();const domObserver = new MutationObserver(updateSpacing);domObserver.observe(pagesContainer, { childList: true, subtree: true });const scrollTarget = findScrollTarget(pagesContainer);scrollTarget?.addEventListener("scroll", updateSpacing, { passive: true });return () => {domObserver.disconnect();scrollTarget?.removeEventListener("scroll", updateSpacing);removeSpacingFromContainer(pagesContainer);};}src/pdfPageSpacing.ts export const PAGE_GAP_PX = 24;const STYLE_TAG_ID = "pdf-page-gap-styles";const SPACING_CLASS_NAME = "pdf-pages-spaced";const PAGE_SELECTOR = '[data-rp="pages"]';function isValidSpacingInput(totalPages: number, gap: number): boolean {return totalPages > 1 && gap > 0;}// Build CSS that adds visible space between PDF pages in the virtual list.//// Margins on page nodes do not work reliably here. Instead:// 1. box shadow below each page shows the gap (viewer background color).// 2. translateY on page 2 and later moves each page down by one more gap.function buildPageGapStyles(totalPages: number, gap: number): string {if (!isValidSpacingInput(totalPages, gap)) return "";const rules: string[] = [`.pdf-pages-spaced [id^="page-"] {box-shadow: 0 ${gap}px 0 0 var(--rp-pages-background-color, #d8d8d8) !important;}`,];// Page 1 stays at y=0; each later page shifts by one more gap.for (let page = 2; page <= totalPages; page++) {const offset = (page - 1) * gap;rules.push(`.pdf-pages-spaced [data-rp="page-${page}"] { transform: translateY(${offset}px) !important; }`,);}return rules.join("\n");}function getOrCreateStyleTag(): HTMLStyleElement {let styleEl = document.getElementById(STYLE_TAG_ID) as HTMLStyleElement | null;if (!styleEl) {styleEl = document.createElement("style");styleEl.id = STYLE_TAG_ID;document.head.appendChild(styleEl);}return styleEl;}function injectPageGapStyles(totalPages: number, gap: number) {const styleEl = getOrCreateStyleTag();styleEl.textContent = buildPageGapStyles(totalPages, gap);}function removePageGapStyles() {document.getElementById(STYLE_TAG_ID)?.remove();}function findGridInner(pagesContainer: HTMLElement): HTMLElement | null {let tallestElement: HTMLElement | null = null;let tallestHeight = 0;for (const el of Array.from(pagesContainer.querySelectorAll<HTMLElement>("div"))) {if (el.style.position !== "relative" || !el.style.height) continue;const height = Number.parseFloat(el.style.height);if (height > tallestHeight) {tallestHeight = height;tallestElement = el;}}return tallestElement;}function adjustInnerScrollHeight(pagesContainer: HTMLElement,gap: number,totalPages: number,) {if (!isValidSpacingInput(totalPages, gap)) return;const inner = findGridInner(pagesContainer);if (!inner) return;if (inner.dataset.originalHeight === undefined && inner.style.height) {inner.dataset.originalHeight = inner.style.height;}const originalHeight = Number.parseFloat(inner.dataset.originalHeight ?? "");if (Number.isNaN(originalHeight)) return;const targetHeight = originalHeight + (totalPages - 1) * gap;if (inner.style.height !== `${targetHeight}px`) {inner.style.height = `${targetHeight}px`;}}function removeSpacingFromContainer(pagesContainer: HTMLElement) {pagesContainer.classList.remove(SPACING_CLASS_NAME);pagesContainer.style.removeProperty("--pdf-page-gap");removePageGapStyles();const inner = findGridInner(pagesContainer);if (inner?.dataset.originalHeight) {inner.style.height = inner.dataset.originalHeight;}}function applySpacingToContainer(pagesContainer: HTMLElement,gap: number,totalPages: number,) {if (gap <= 0 || totalPages <= 1) {removeSpacingFromContainer(pagesContainer);return;}pagesContainer.style.setProperty("--pdf-page-gap", `${gap}px`);pagesContainer.classList.add(SPACING_CLASS_NAME);injectPageGapStyles(totalPages, gap);adjustInnerScrollHeight(pagesContainer, gap, totalPages);}function findScrollTarget(pagesContainer: HTMLElement): HTMLElement | null {return (pagesContainer.querySelector<HTMLElement>('[style*="overflow"]') ??pagesContainer.parentElement);}export function findPagesContainer(root: HTMLElement): HTMLElement | null {return root.querySelector<HTMLElement>(PAGE_SELECTOR);}export function enablePageSpacing(pagesContainer: HTMLElement,gap: number,getTotalPages: () => number,) {const updateSpacing = () => {const totalPages = getTotalPages();if (totalPages <= 0) return;applySpacingToContainer(pagesContainer, gap, totalPages);};updateSpacing();const domObserver = new MutationObserver(updateSpacing);domObserver.observe(pagesContainer, { childList: true, subtree: true });const scrollTarget = findScrollTarget(pagesContainer);scrollTarget?.addEventListener("scroll", updateSpacing, { passive: true });return () => {domObserver.disconnect();scrollTarget?.removeEventListener("scroll", updateSpacing);removeSpacingFromContainer(pagesContainer);};} -
Sync spacing with the loaded PDF
This component waits for the document to load, then calls
enablePageSpacing.src/PageSpacing.jsx import { useDocumentContext } from "@react-pdf-kit/viewer";import { useEffect, useRef } from "react";import { enablePageSpacing, findPagesContainer } from "./pdfPageSpacing";export function PageSpacing({ viewerRef, gap }) {const { pdf, loading } = useDocumentContext();const cleanupSpacingRef = useRef(null);const clearSpacing = () => {cleanupSpacingRef.current?.();cleanupSpacingRef.current = null;};useEffect(() => {if (loading || !pdf) return;const totalPages = pdf.numPages;const trySetupSpacing = () => {const root = viewerRef.current;if (!root) return false;const pagesContainer = findPagesContainer(root);if (!pagesContainer) return false;clearSpacing();cleanupSpacingRef.current = enablePageSpacing(pagesContainer,gap,() => totalPages,);return true;};if (trySetupSpacing()) {return () => {clearSpacing();};}let attempts = 0;let frameId = 0;const retry = () => {if (trySetupSpacing() || attempts >= 120) return;attempts += 1;frameId = requestAnimationFrame(retry);};frameId = requestAnimationFrame(retry);return () => {cancelAnimationFrame(frameId);clearSpacing();};}, [pdf, loading, gap, viewerRef]);return null;}src/PageSpacing.tsx import { useDocumentContext } from "@react-pdf-kit/viewer";import { useEffect, useRef, type RefObject } from "react";import { enablePageSpacing, findPagesContainer } from "./pdfPageSpacing";type PageSpacingProps = {viewerRef: RefObject<HTMLElement | null>;gap: number;};export function PageSpacing({ viewerRef, gap }: PageSpacingProps) {const { pdf, loading } = useDocumentContext();const cleanupSpacingRef = useRef<(() => void) | null>(null);const clearSpacing = () => {cleanupSpacingRef.current?.();cleanupSpacingRef.current = null;};useEffect(() => {if (loading || !pdf) return;const totalPages = pdf.numPages;const trySetupSpacing = (): boolean => {const root = viewerRef.current;if (!root) return false;const pagesContainer = findPagesContainer(root);if (!pagesContainer) return false;clearSpacing();cleanupSpacingRef.current = enablePageSpacing(pagesContainer,gap,() => totalPages,);return true;};if (trySetupSpacing()) {return () => {clearSpacing();};}let attempts = 0;let frameId = 0;const retry = () => {if (trySetupSpacing() || attempts >= 120) return;attempts += 1;frameId = requestAnimationFrame(retry);};frameId = requestAnimationFrame(retry);return () => {cancelAnimationFrame(frameId);clearSpacing();};}, [pdf, loading, gap, viewerRef]);return null;} -
Gap input
This component lets users pick a gap from 0 to 200 px.
src/PageGapControl.jsx const MIN_GAP = 0;const MAX_GAP = 200;function parseGapInput(value) {const parsed = Number.parseInt(value, 10);if (Number.isNaN(parsed)) return MIN_GAP;return Math.min(MAX_GAP, Math.max(MIN_GAP, parsed));}export function PageGapControl({ gap, onGapChange }) {return (<labelstyle={{display: "flex",alignItems: "center",gap: "8px",padding: "8px 12px",fontSize: "14px",borderBottom: "1px solid #e5e4e7",background: "#fff",}}>Page spacing (px)<inputtype="number"min={MIN_GAP}max={MAX_GAP}step={1}value={gap}onChange={(event) => onGapChange(parseGapInput(event.target.value))}style={{width: "72px",padding: "4px 8px",fontSize: "14px",}}/></label>);}src/PageGapControl.tsx type PageGapControlProps = {gap: number;onGapChange: (gap: number) => void;};const MIN_GAP = 0;const MAX_GAP = 200;function parseGapInput(value: string): number {const parsed = Number.parseInt(value, 10);if (Number.isNaN(parsed)) return MIN_GAP;return Math.min(MAX_GAP, Math.max(MIN_GAP, parsed));}export function PageGapControl({ gap, onGapChange }: PageGapControlProps) {return (<labelstyle={{display: "flex",alignItems: "center",gap: "8px",padding: "8px 12px",fontSize: "14px",borderBottom: "1px solid #e5e4e7",background: "#fff",}}>Page spacing (px)<inputtype="number"min={MIN_GAP}max={MAX_GAP}step={1}value={gap}onChange={(event) => onGapChange(parseGapInput(event.target.value))}style={{width: "72px",padding: "4px 8px",fontSize: "14px",}}/></label>);} -
Wire the viewer
This step connects the gap state to the PDF viewer.
src/App.jsx import { useRef, useState } from "react";import {RPConfig,RPLayout,RPPages,RPProvider,} from "@react-pdf-kit/viewer";import { PageGapControl } from "./PageGapControl";import { PageSpacing } from "./PageSpacing";import { PAGE_GAP_PX } from "./pdfPageSpacing";function App() {const viewerRef = useRef(null);const [pageGap, setPageGap] = useState(PAGE_GAP_PX);return (<divref={viewerRef}style={{width: "100%",height: "700px",display: "flex",flexDirection: "column",}}><h1 style={{ textAlign: "center" }}>Adjust Space Between Pages</h1><PageGapControl gap={pageGap} onGapChange={setPageGap} /><div style={{ flex: 1, minHeight: 0 }}><RPConfig><RPProvider src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"><PageSpacing viewerRef={viewerRef} gap={pageGap} /><RPLayout toolbar style={{ width: "100%", height: "100%" }}><RPPages /></RPLayout></RPProvider></RPConfig></div></div>);}export default App;src/App.tsx import { useRef, useState } from "react";import {RPConfig,RPLayout,RPPages,RPProvider,} from "@react-pdf-kit/viewer";import { PageGapControl } from "./PageGapControl";import { PageSpacing } from "./PageSpacing";import { PAGE_GAP_PX } from "./pdfPageSpacing";function App() {const viewerRef = useRef<HTMLDivElement>(null);const [pageGap, setPageGap] = useState(PAGE_GAP_PX);return (<divref={viewerRef}style={{width: "100%",height: "700px",display: "flex",flexDirection: "column",}}><h1 style={{ textAlign: "center" }}>Adjust Space Between Pages</h1><PageGapControl gap={pageGap} onGapChange={setPageGap} /><div style={{ flex: 1, minHeight: 0 }}><RPConfig><RPProvider src="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"><PageSpacing viewerRef={viewerRef} gap={pageGap} /><RPLayout toolbar style={{ width: "100%", height: "100%" }}><RPPages /></RPLayout></RPProvider></RPConfig></div></div>);}export default App;
- Render
PageSpacinginsideRPProvidersouseDocumentContextcan access the open file. - This approach depends on React PDF Kit
data-rpDOM attributes.