A lightweight, standalone JavaScript PDF viewer library that wraps Mozilla's pdf.js library with a modular course/chapter interface and responsive UI, making it easy to integrate into your web applications.
- Quick Start
- Installation
- Core Concepts
- Basic Usage
- API Reference
- Course Data Format
- Customization
- Troubleshooting
- Examples
- Browser Support
- Performance Tips
- Limitations & Known Issues
Get running in 5 minutes:
Important: The library exports as
PDFViewer, notSimplePDFviewer. Always usePDFViewer.init()to initialize the viewer.
-
Add the script to your HTML:
<div id="viewer" style="height: 100vh;"></div> <script src="https://cdn.jsdelivr.net/gh/BrunoRNS/SimplePDFviewer@latest/min/core.min.js"></script> <script> const course = { title: "My Course", modules: [{ title: "Module 1", chapters: [{ title: "Chapter 1", pdf: "path/to/pdf.pdf" }] }] }; PDFViewer.init(document.getElementById('viewer'), course); </script>
-
Style the container (set height and width):
<style> #viewer { height: 100vh; width: 100%; } </style>
That's it! You now have a fully functional PDF viewer with navigation and chapter sidebar.
<script src="https://cdn.jsdelivr.net/gh/BrunoRNS/SimplePDFviewer@latest/min/core.min.js"></script>Download min/core.min.js from the repository and include it:
<script src="./core.min.js"></script>For development, use the unminified version from src/:
<script src="./src/SimplePDFviewer.js"></script>SimplePDFviewer organizes PDFs into a hierarchical structure:
- Course: The root container with a title and modules
- Module: A collection of chapters (e.g., "Chapter 1", "Chapter 2")
- Chapter: Individual PDF documents with titles and URLs
This structure allows you to organize related PDFs and navigate between them seamlessly.
- Sidebar: Lists all chapters for quick access
- Previous/Next buttons: Navigate pages within a PDF or jump to the next/previous chapter
- Keyboard navigation: Use arrow keys (←/→) to navigate
- Responsive design: Sidebar collapses on mobile, toggleable with menu button
- Auto-scaling: PDF pages automatically scale to fit the container
- Automatic zoom reset: Zoom automatically resets to 100% when navigating between pages or chapters
- Automatic scroll reset: Page scroll position automatically resets to top-left when navigating between pages or chapters
const course = {
title: "Introduction to JavaScript",
modules: [
{
title: "Fundamentals",
chapters: [
{
title: "Variables & Types",
pdf: "https://example.com/variables.pdf"
},
{
title: "Functions",
pdf: "https://example.com/functions.pdf"
}
]
},
{
title: "Advanced Topics",
chapters: [
{
title: "Closures",
pdf: "https://example.com/closures.pdf"
}
]
}
]
};const viewer = PDFViewer.init(
document.getElementById('viewer'),
course,
{
onError: (error) => {
console.error('PDF Error:', error);
}
}
);// Navigate to next page
viewer.nextPage();
// Navigate to previous page
viewer.prevPage();
// Load a specific chapter
viewer.loadChapter(0, 1); // Module 0, Chapter 1
// Access current state
console.log(viewer.currentModule); // 0
console.log(viewer.currentChapter); // 0
console.log(viewer.currentPage); // 1
console.log(viewer.pdfDoc); // pdf.js document object
// Clean up when done
viewer.destroy();Initializes a PDF viewer instance.
Parameters:
container(HTMLElement, required): The DOM element where the viewer will be renderedcourse(Object, required): Course data object with structure detailed in Course Data Formatoptions(Object, optional): Configuration optionsonError(Function): Callback function for error handling. Called with error object:{ type, message, error }colorTheme(String): Optional hex color for custom theme (e.g.,#FF5722). Auto-adjusts if too bright/dark. Colors are auto-calculated for all UI elements.
Returns:
Instance object or null if initialization fails.
Example:
const viewer = PDFViewer.init(
document.getElementById('viewer'),
courseData,
{
onError: (err) => {
console.error(`${err.type} error: ${err.message}`);
}
}
);
if (!viewer) {
console.error('Failed to initialize viewer');
}Navigate to the next page. If at the end of a chapter, loads the next chapter.
viewer.nextPage();Navigate to the previous page. If at the beginning of a chapter, loads the previous chapter.
viewer.prevPage();Load a specific chapter by module and chapter indices.
viewer.loadChapter(0, 2); // Load Chapter 2 from Module 0
if (viewer.pdfDoc) {
console.log(`Loaded ${viewer.pdfDoc.numPages} pages`);
}Clean up the viewer instance. Removes event listeners and cancels rendering tasks.
viewer.destroy();Set the zoom level for the current page (100-200%).
Parameters:
value(Number): Zoom level between 100 (100%) and 200 (200%)
Example:
// Zoom to 150%
viewer.setZoom(150);
// Current zoom level
console.log(viewer.zoom); // e.g., 150Behavior:
- Automatically clamps value to 100-200 range
- Preserves scroll position proportionally when possible
- Updates UI controls immediately
- Page is re-rendered at the new zoom level
Note: Zoom automatically resets to 100% when navigating to a different page or chapter for better UX.
Enable or disable text selection at runtime. Text selection is enabled by default and allows users to select and copy text from PDFs.
Parameters:
enabled(Boolean):trueto enable text selection,falseto disable
Example:
// Disable text selection
viewer.setTextSelectionEnabled(false);
// Re-enable text selection
viewer.setTextSelectionEnabled(true);
// Toggle based on user preference
document.getElementById('toggle-selection').onclick = () => {
viewer.setTextSelectionEnabled(!viewer.enableTextSelection);
};
// Check current state
if (viewer.enableTextSelection) {
console.log('Text selection is enabled');
}Configuration at Initialization:
// Disable text selection from the start
const viewer = PDFViewer.init(container, course, {
enableTextSelection: false
});
// Default behavior (enabled)
const viewer2 = PDFViewer.init(container, course);How It Works:
- Text selection uses a transparent text layer positioned on top of the PDF canvas
- Users can highlight and copy text without affecting the visual PDF rendering
- The text layer is automatically rendered when each page loads
- Text selection can be toggled at any time without reloading the PDF
Change the theme color after initialization. Automatically calculates all UI colors from the provided hex color.
Parameters:
hex(String): Hex color code (e.g.,#FF5722,#F52)
Returns: true if successful, false if invalid format or lightness out of range
Example:
// Change to pink theme
if (viewer.setTheme('#E91E63')) {
console.log('Theme changed successfully');
} else {
console.error('Invalid color format');
}
// With preview functionality
document.getElementById('colorPicker').addEventListener('change', (e) => {
viewer.setTheme(e.target.value);
});Behavior:
- Auto-adjusts very dark colors (< 15% lightness) to #2a2a2a minimum
- Auto-adjusts very bright colors (> 85% lightness) to #e8e8e8 maximum
- Maintains hue and saturation while adjusting lightness when needed
- Re-injects CSS to update all UI colors immediately
currentModule(number): Index of current module (0-based)currentChapter(number): Index of current chapter (0-based)currentPage(number): Current page number (1-based)zoom(number): Current zoom level (100-200, default 100). Automatically resets to 100 when navigating between pages or chaptersenableTextSelection(boolean): Whether text selection is currently enabled (default: true)pdfDoc(pdfjsLib.PDFDocument): The pdf.js document object (null before loading)renderTask(pdfjsLib.RenderTask): Current render task (null if not rendering)onError(Function): Error callback functionthemeColors(Object): Current theme colors object with properties likeprimary,primaryHover,primaryActive,sidebarPrimary, etc.
When navigating between pages or chapters using nextPage(), prevPage(), or loadChapter(), the viewer automatically applies the following state resets for optimal UX:
- Zoom: Resets to 100% to ensure the entire page is visible by default
- Scroll Position: Resets to top-left (0, 0) so the new page starts at the beginning
- UI State: Previous/Next buttons update based on current position in the course
- Scroll reset uses multiple simultaneous methods to ensure reliability across browsers
- Scroll restoration and zoom restoration happen after page rendering completes
- These behaviors are transparent to the user and cannot be disabled
const viewer = PDFViewer.init(container, course);
// Listen for manual navigation
document.getElementById('next-btn').onclick = () => {
viewer.nextPage(); // Auto-resets zoom and scroll
console.log(`Zoom: ${viewer.zoom}%, Page: ${viewer.currentPage}`);
};
// Jump to a chapter - also triggers auto-reset
document.getElementById('jump-chapter').onclick = () => {
viewer.loadChapter(1, 0); // Zoom and scroll auto-reset
};The course object must follow this structure:
{
title: "string", // Required: Course title
modules: [ // Required: Array of modules
{
title: "string", // Required: Module title
chapters: [ // Required: Array of chapters
{
title: "string", // Required: Chapter title
pdf: "string" // Required: PDF URL (must be accessible/CORS-enabled)
},
// ... more chapters
]
},
// ... more modules
]
}title: Must be a non-empty stringmodules: Must be a non-empty array- Each module must have a
titleandchaptersarray - Each chapter must have a
titleandpdfURL - All required fields are validated at initialization
You can add extra properties for your own tracking:
const course = {
id: "course-001",
title: "Advanced JavaScript",
modules: [
{
id: "mod-101",
title: "Core Concepts",
chapters: [
{
id: "ch-1001",
title: "Chapter 1",
pdf: "https://example.com/ch1.pdf"
}
]
}
]
};SimplePDFviewer injects default CSS classes. Override them with your own styles:
<style>
/* Container and layout */
.pdf-viewer-container { /* Main container */ }
.pdf-viewer-main { /* Main content area */ }
.pdf-viewer-sidebar { /* Left sidebar */ }
.pdf-viewer-controls { /* Top control bar */ }
.pdf-viewer-canvas-container { /* Canvas wrapper */ }
/* Canvas and loading */
.pdf-viewer-canvas { /* PDF canvas */ }
.pdf-viewer-loading-overlay { /* Loading indicator */ }
/* Chapters list */
.pdf-viewer-chapter-item { /* Chapter in sidebar */ }
.pdf-viewer-chapter-item:hover { /* Chapter hover state */ }
.pdf-viewer-chapter-item.active { /* Active chapter */ }
/* Buttons */
.pdf-viewer-btn { /* All buttons */ }
.pdf-viewer-btn:hover { /* Button hover */ }
.pdf-viewer-btn:disabled { /* Disabled button */ }
.pdf-viewer-toggle-btn { /* Menu toggle button */ }
</style>.pdf-viewer-sidebar {
background: #1a1a1a; /* Dark sidebar */
color: #ffffff;
}
.pdf-viewer-btn {
background: #0066cc; /* Custom blue */
}
.pdf-viewer-btn:hover {
background: #0052a3;
}
.pdf-viewer-chapter-item.active {
background: #0066cc;
}Use the new colorTheme option to automatically generate all UI colors from a single hex color. Colors are intelligently calculated to maintain proper contrast and visual hierarchy.
const viewer = PDFViewer.init(
document.getElementById('viewer'),
course,
{
colorTheme: '#FF5722' // Use Material Design Deep Orange
}
);const viewer = PDFViewer.init(container, course);
// Change theme later
viewer.setTheme('#E91E63'); // Pink
viewer.setTheme('#4CAF50'); // Green
viewer.setTheme('#2196F3'); // Blue
viewer.setTheme('#FF9800'); // Orange- Provides a single hex color as the primary theme color
- Library automatically calculates:
- Darker shades for hover and active states
- Muted/darker sidebar color with reduced saturation
- Maintains proper contrast for accessibility
- Uses HSL color space for intelligent scaling
The system automatically adjusts colors that are too bright or too dark:
viewer.setTheme('#000000'); // Auto-adjusted to #2a2a2a (minimum usable darkness)
viewer.setTheme('#FFFFFF'); // Auto-adjusted to #e8e8e8 (maximum usable brightness)
viewer.setTheme('#FF5722'); // Normal - accepted as-isReturns: true if successful, false if invalid format
Accepted formats:
#RRGGBB(e.g.,#FF5722)#RGB(e.g.,#F52)
Invalid formats (will be rejected):
rgb(255, 87, 34)FF5722(missing #)#GGHHII(invalid hex)
// HTML
<button onclick="changeTheme('#FF5722')">Orange</button>
<button onclick="changeTheme('#E91E63')">Pink</button>
<input type="text" id="colorInput" placeholder="#FF5722">
<button onclick="applyCustomColor()">Apply</button>
// JavaScript
function applyCustomColor() {
const color = document.getElementById('colorInput').value;
if (viewer.setTheme(color)) {
alert(`Theme changed to ${color}`);
} else {
alert(`Invalid color: ${color}`);
}
}<style>
#viewer {
height: 800px;
width: 100%;
border: 2px solid #ccc;
}
</style>const viewer = PDFViewer.init(container, course, {
onError: (error) => {
const { type, message, error: err } = error;
switch(type) {
case 'load':
console.error('Failed to load PDF:', message);
break;
case 'render':
console.error('Failed to render page:', message);
break;
default:
console.error('Error:', message);
}
// You could show a user-friendly message
if (type === 'load') {
showNotification('Failed to load PDF. Please try again.');
}
}
});Symptoms: Error alert after clicking a chapter, "Error loading PDF" message
Solutions:
-
Check CORS: The PDF URL must be CORS-enabled. If hosting on a different domain, configure your server:
Access-Control-Allow-Origin: * -
Verify URL: Ensure the PDF URL is correct and accessible:
// Test in browser console fetch('your-pdf-url').then(r => console.log(r.status))
-
Browser Console: Check browser console for detailed error messages (F12 → Console tab)
Symptoms: Chapters sidebar is hidden on mobile devices
Expected behavior: The sidebar is hidden by default on mobile (<768px width) and can be toggled with the menu button (☰).
Solution: Use the hamburger menu button to toggle the sidebar, or increase your browser window width.
Symptoms: Console error about "pdf.worker.min.js"
Possible causes:
- CDN is blocked or inaccessible
- Script is hosted offline without CORS configuration
Solution:
- Check internet connection
- Check browser console for network errors
- For offline use, download both
pdf.min.jsandpdf.worker.min.jsand update URLs
Symptoms: Blank canvas, blurry text, or rendering errors
Solutions:
- Verify PDF validity: The PDF might be corrupted. Test with a known-good PDF
- Check container size: Ensure the container has dimensions (height/width)
- Clear browser cache:
Ctrl+Shift+Delete(orCmd+Shift+Deleteon Mac)
Symptoms: Browser feels slow, high CPU usage in Task Manager
Possible causes:
- Rapidly resizing window
- Large PDF with many pages
Solutions:
- Debouncing is built in, but rendering triggers on resize. This is expected behavior for dynamic sizing.
- Use smaller PDFs or convert large PDFs to multiple chapters
Load PDFs from your local server:
const course = {
title: "Training Materials",
modules: [
{
title: "Week 1",
chapters: [
{ title: "Monday", pdf: "/pdfs/monday.pdf" },
{ title: "Tuesday", pdf: "/pdfs/tuesday.pdf" },
{ title: "Wednesday", pdf: "/pdfs/wednesday.pdf" }
]
},
{
title: "Week 2",
chapters: [
{ title: "Thursday", pdf: "/pdfs/thursday.pdf" },
{ title: "Friday", pdf: "/pdfs/friday.pdf" }
]
}
]
};
PDFViewer.init(document.getElementById('viewer'), course);const viewer = PDFViewer.init(container, course, {
onError: (error) => {
const message = error.type === 'load'
? 'Unable to load PDF. Check your internet connection.'
: 'Failed to display PDF. Please try with another browser.';
document.getElementById('error-message').textContent = message;
}
});// Load course data from API
fetch('/api/course')
.then(res => res.json())
.then(course => {
const viewer = PDFViewer.init(
document.getElementById('viewer'),
course
);
});const course1 = { /* ... */ };
const course2 = { /* ... */ };
const viewer1 = PDFViewer.init(
document.getElementById('viewer1'),
course1
);
const viewer2 = PDFViewer.init(
document.getElementById('viewer2'),
course2
);
// Navigate both independently
viewer1.nextPage();
viewer2.loadChapter(0, 1);const viewer = PDFViewer.init(container, course);
// Custom button controls
document.getElementById('jump-to-chapter-2').onclick = () => {
viewer.loadChapter(0, 2);
};
// Display current progress
document.getElementById('progress').textContent =
`Module ${viewer.currentModule + 1}, ` +
`Chapter ${viewer.currentChapter + 1}, ` +
`Page ${viewer.currentPage} of ${viewer.pdfDoc?.numPages || '?'}`;| Browser | Version | Status |
|---|---|---|
| Chrome | 45+ | Full support |
| Firefox | 40+ | Full support |
| Safari | 10+ | Full support |
| Edge | 12+ | Full support |
| IE | 11 | Limited (no canvas HiDPI) |
| Mobile Chrome | Latest 2 | Full support |
| Mobile Safari | Latest 2 | Full support |
Note: Uses ResizeObserver and other modern APIs. For older browsers, consider using polyfills.
- Compress PDFs: Use tools like ImageMagick or GhostScript to reduce file size
- Remove unnecessary content: Delete unused fonts, images, and annotations
- Split large documents: Break very large PDFs into multiple chapters
Print-to-PDF usually compresses well. For web-native PDFs, ensure your server sends proper cache headers:
Cache-Control: public, max-age=31536000- Use a CDN to serve PDFs from servers closest to users
- Compress responses with gzip
- Don't preload all PDFs, load on-demand
- Consider showing a preview or loading spinner
- Call
viewer.destroy()when done with a viewer instance - Close unused viewer instances
- For multiple viewers, limit active instances
- No search functionality (use pdf.js directly if needed)
- No annotations (view-only)
- Performance degrades with very large PDFs (500+ pages per chapter)
- CORS requirement: PDFs must be from CORS-enabled servers
- Worker requirement: pdf.js worker must be accessible
- Mobile sidebar: Sidebar auto-closes after chapter selection for UX
- High-DPI rendering: Slightly higher memory usage on Retina/4K displays
For CSS-based zoom scaling (alternative to built-in controls):
.pdf-viewer-canvas-container {
transform: scale(1.2);
transform-origin: top center;
}Note: Built-in zoom controls (100-200%) are now available and are recommended for better UX.
For search: Consider using the full pdf.js library or adding a search layer over the viewer.
- GitHub Issues: Issues
- Documentation: SimplePDFviewer Home Page
- pdf.js Docs: PDF.js Home Page
MIT License - See LICENSE.txt for details
This project, SimplePDFviewer, is not affiliated with Mozilla, the developers of the pdf.js library that it uses. While the project is open-source and free to use, it is not officially endorsed or supported by Mozilla. If you have any issues or need help with the viewer, please refer to the GitHub issues page or the documentation provided in the repository.