Quickly view and compare PDFs, Office documents, and images with a side-by-side layout. Keep both documents in sync as you scroll and zoom for a seamless comparison experience.
This demo allows you to:
Implementation steps
To add Side-by-Side viewing capability with WebViewer:
Step 1: Get started with WebViewer in your preferred web stack.
Step 2: Add the ES6 JavaScript sample code provided in this guide.
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
1// ES6 Compliant Syntax
2// GitHub Copilot, Claude Sonnet 4 (Preview), October 16, 2025
3// File: showcase-demos/side-by-side/index.js
4import WebViewer from '@pdftron/webviewer';
5
6const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
7
8// Global variables to track state
9const defaultDoc1 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/demo-annotated.pdf';
10const defaultDoc2 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf';
11let syncScroll = false;
12let isUpdatingSync = false; // Guard flag to prevent circular calls
13let file1 = null;
14let file2 = null;
15let title1 = '';
16let title2 = '';
17let canStartComparing = false;
18
19// Store references for cleanup
20let documentViewer1 = null;
21let documentViewer2 = null;
22let multiViewerReadyHandler = null;
23let documentLoadedHandler1 = null;
24let documentLoadedHandler2 = null;
25
26// Function to initialize and load the Redaction Tool
27function initializeWebViewer() {
28
29 const element = document.getElementById('viewer');
30 if (!element) {
31 console.error('Viewer div not found.');
32 return;
33 }
34
35 WebViewer({
36 path: '/lib',
37 licenseKey: licenseKey,
38 }, element).then(instance => {
39 // Get Core and UI from the instance
40 const { Core, UI } = instance;
41
42 createUIElements();
43 UI.enableElements(['contentEditButton']);
44 UI.disableElements(['comparisonToggleButton']);
45 UI.enableFeatures([UI.Feature.MultiViewerMode]);
46 UI.enterMultiViewerMode();
47 // Create the multi-viewer ready handler
48 multiViewerReadyHandler = () => {
49 // Now both document viewers are available
50 documentViewer1 = Core.getDocumentViewers()[0];
51 documentViewer2 = Core.getDocumentViewers()[1];
52
53 // Create document loaded handlers
54 documentLoadedHandler1 = () => {
55 file1 = documentViewer1.getDocument();
56 };
57
58 documentLoadedHandler2 = () => {
59 file2 = documentViewer2.getDocument();
60 };
61
62 // Add document loaded event listeners
63 documentViewer1.addEventListener('documentLoaded', documentLoadedHandler1);
64 documentViewer2.addEventListener('documentLoaded', documentLoadedHandler2);
65
66 // Load default documents into both viewers
67 documentViewer1?.loadDocument(defaultDoc1);
68 documentViewer2?.loadDocument(defaultDoc2);
69
70 // Add event listeners to sync scroll & zoom button in both viewers
71 addSyncListener(documentViewer1, UI, syncScroll);
72 addSyncListener(documentViewer2, UI, syncScroll);
73 };
74
75 // Set up multi-viewer ready event
76 UI.addEventListener(UI.Events.MULTI_VIEWER_READY, multiViewerReadyHandler);
77
78 }).catch(error => {
79 console.error('Error initializing WebViewer:', error);
80 });
81}
82
83// Add event listener to sync scroll & zoom button in multi-viewer mode
84function addSyncListener(
85 documentViewer,
86 UI,
87 syncScroll
88) {
89 documentViewer
90 ?.getViewerElement()
91 ?.closest('.CompareContainer')
92 // The first button is the "Start Sync" button
93 ?.querySelector('.control-buttons button')
94 ?.addEventListener('click', () => {
95 setTimeout(() => {
96 syncScroll = UI.isMultiViewerSyncing();
97 setSyncScroll(syncScroll);
98 isUpdatingSync = true;
99 UIElements.updateSyncToggleUIOnly(syncScroll);
100 }, 0);
101 });
102};
103
104// Function to sync up document viewers to scroll and zoom together
105function setSyncScroll(value) {
106 syncScroll = value;
107 toggleSyncScrollZoom(syncScroll);
108}
109
110// Function to enable/disable sync scroll & zoom
111function toggleSyncScrollZoom(enable) {
112 if (enable) {
113 window.WebViewer.getInstance().UI.enableMultiViewerSync();
114 console.log('Sync scroll & zoom enabled');
115 syncScroll = true;
116 } else {
117 window.WebViewer.getInstance().UI.disableMultiViewerSync();
118 console.log('Sync scroll & zoom disabled');
119 syncScroll = false;
120 }
121}
122
123// Expose file variables and functions to the global scope for UI interaction
124window.toggleSyncScrollZoom = toggleSyncScrollZoom;
125window.isUpdatingSync = isUpdatingSync;
126window.file1 = file1;
127window.file2 = file2;
128window.title1 = title1;
129window.title2 = title2;
130window.canStartComparing = canStartComparing;
131
132// UI Elements
133// Function to create and initialize UI elements
134function createUIElements() {
135 // Create a container for all controls (label, dropdown, and buttons)
136 // Dynamically load ui-elements.js if not already loaded
137 if (!window.SidePanel) {
138 const script = document.createElement('script');
139 script.src = '/showcase-demos/side-by-side/ui-elements.js';
140 script.onload = () => {
141 UIElements.init('viewer');
142 };
143 document.head.appendChild(script);
144 }
145}
146
147// Initialize the WebViewer
148initializeWebViewer();
149
1// ES6 Compliant Syntax
2// GitHub Copilot, Claude Sonnet 4 (Preview), October 16, 2025
3// File: showcase-demos/side-by-side/ui-elements.js
4
5class UIElements {
6
7 static init(viewerId) {
8 this.createSidePanel(viewerId);
9 this.setupEventHandlers();
10 }
11
12 // Function to create a side panel that sits on the left side of the viewer
13 static createSidePanel(viewerId) {
14 const viewerElement = document.getElementById(viewerId);
15 if (!viewerElement) {
16 console.error(`Viewer element with id '${viewerId}' not found.`);
17 return;
18 }
19
20 // Create the side panel container
21 const sidePanel = document.createElement('div');
22 sidePanel.id = 'side-panel';
23 sidePanel.className = 'side-panel';
24
25 // Create side panel content
26 const content = document.createElement('div');
27 content.className = 'side-panel-content';
28
29 // Add the text extraction content
30 const sampleContent = document.createElement('div');
31 sampleContent.innerHTML = `
32 <div class="panel-section">
33 <h4>Side by Side Demo Files</h4>
34
35 <!-- File Version A (Left) -->
36 <div id="file-version-a">
37 <h3>File Version A (Left)</h3>
38 <input type="text" readonly placeholder="demo-annotated.pdf" class="file-input">
39 <button class="select-file-btn" id="btn-file-version-a">Select File</button>
40 </div>
41
42 <!-- File Version B (Right) -->
43 <div id="file-version-b">
44 <h3>File Version B (Right)</h3>
45 <input type="text" readonly placeholder="WebViewerDemoDoc.pdf" class="file-input">
46 <button class="select-file-btn" id="btn-file-version-b">Select File</button>
47 </div>
48
49 <!-- Sync Toggle -->
50 <div id="sync-toggle">
51 <input type="range" min="0" max="1" value="0" class="sync-slider" id="sync-slider">
52 <br>
53 <label for="sync-slider">Sync Scroll & Zoom</label>
54 </div>
55
56 </div>
57 `;
58
59 content.appendChild(sampleContent);
60 sidePanel.appendChild(content);
61
62 // Create a wrapper to contain both the side panel and viewer
63 const wrapper = document.createElement('div');
64 wrapper.id = 'viewer-wrapper';
65 wrapper.className = 'viewer-wrapper';
66
67 // Insert the wrapper before the viewer element
68 viewerElement.parentNode.insertBefore(wrapper, viewerElement);
69
70 // Move the viewer element into the wrapper and add the side panel
71 wrapper.appendChild(sidePanel);
72 wrapper.appendChild(viewerElement);
73
74 // Add the viewer-with-panel class to the viewer element
75 viewerElement.classList.add('viewer-with-panel');
76 }
77
78 // Function to add content to the side panel
79 addPanelContent(content) {
80 const panelContent = document.querySelector('.side-panel-content');
81 if (panelContent) {
82 const contentDiv = document.createElement('div');
83 contentDiv.className = 'panel-section';
84 contentDiv.innerHTML = content;
85 panelContent.appendChild(contentDiv);
86 }
87 }
88
89 static updateSyncToggleUIOnly(syncScroll) {
90 window.isUpdatingSync = true;
91 const syncSlider = document.getElementById('sync-slider');
92 if (syncSlider) {
93 // Guard against circular calls
94 syncSlider.value = syncScroll ? '1' : '0';
95 console.log('Sync slider UI updated to:', syncSlider.value);
96 // Function to update slider background color
97 const updateSliderBackground = (value) => {
98 if (value === '1') {
99 // Toggled to the right - light blue background
100 syncSlider.style.backgroundColor = '#87CEEB';
101 } else {
102 // Toggled to the left - gray background
103 syncSlider.style.backgroundColor = '#dadee2';
104 }
105 };
106 // Set initial background color
107 updateSliderBackground(syncSlider.value);
108 toggleSyncScrollZoom(syncScroll);
109 console.log('Sync slider background color updated to:', syncSlider.style.backgroundColor);
110 console.log('Sync toggle UI only updated to:', syncScroll);
111 console.log('syncSlider.value:', syncSlider.value);
112 }
113 }
114 // Setup event handlers for UI elements
115 static setupEventHandlers() {
116 // Sync toggle slider event listener
117 const syncSlider = document.getElementById('sync-slider');
118 if (syncSlider) {
119 // Function to update slider background color
120 const updateSliderBackground = (value) => {
121 if (value === '1') {
122 // Toggled to the right - light blue background
123 syncSlider.style.backgroundColor = '#87CEEB';
124 } else {
125 // Toggled to the left - gray background
126 syncSlider.style.backgroundColor = '#dadee2';
127 }
128 };
129
130 // Set initial background color
131 updateSliderBackground(syncSlider.value);
132
133 syncSlider.addEventListener('change', (event) => {
134 const isSync = event.target.value === '1';
135 // Update background color
136 updateSliderBackground(event.target.value);
137
138 // Guard against circular calls
139 if (window.isUpdatingSync) {
140 window.isUpdatingSync = false;
141 }
142
143 // Call the toggleSyncScrollZoom function in index.js if it exists
144 if (window.toggleSyncScrollZoom && typeof window.toggleSyncScrollZoom === 'function') {
145 window.toggleSyncScrollZoom(isSync);
146 }
147
148 });
149
150 // Also listen for input event for real-time updates while dragging
151 syncSlider.addEventListener('input', (event) => {
152 updateSliderBackground(event.target.value);
153 });
154 }
155
156 // File selection button event listeners
157 const selectFileButtons = document.querySelectorAll('.select-file-btn');
158 selectFileButtons.forEach((button, index) => {
159 button.addEventListener('click', () => {
160
161 if (button.id === 'btn-file-version-a') {
162 UIElements.filePicker('A');
163 }
164 else if (button.id === 'btn-file-version-b') {
165 UIElements.filePicker('B');
166 }
167 else if (button.id === 'sync-slider') {
168 window.toggleSyncScrollZoom((index === 1 ? true : false));
169 }
170 });
171 });
172 }
173
174 // Method to handle file upload for either side
175 static filePicker(selectSide) {
176 console.log(`Upload file for side: ${selectSide}`);
177
178 const instance = window.WebViewer.getInstance();
179 const viewerId = selectSide === 'A' ? 0 : 1;
180
181 // Create and trigger file input dialog directly
182 const input = document.createElement('input');
183 input.type = 'file';
184 input.accept = '.pdf,.docx,.doc,.pptx,.ppt,.xlsx,.xls,.txt,.rtf,.html,.htm,.xml,.tiff,.tif,.jpg,.jpeg,.png,.bmp,.gif,.svg,.webp,.heic,.heif';
185
186 input.onchange = async (event) => {
187 const file = event.target.files[0];
188 if (file) {
189 try {
190 const doc = await instance.Core.createDocument(file);
191
192 if (viewerId === 0) {
193 if (window.file1) {
194 window.file1.unloadResources();
195 }
196 window.file1 = doc;
197 instance.Core.getDocumentViewers()[0].loadDocument(doc);
198 window.title1 = file.name || 'File A';
199 console.log('Loaded file A:', window.title1);
200 } else {
201 if (window.file2) {
202 window.file2.unloadResources();
203 }
204 window.file2 = doc;
205 instance.Core.getDocumentViewers()[1].loadDocument(doc);
206 window.title2 = file.name || 'File B';
207 console.log('Loaded file B:', window.title2);
208 }
209
210 window.canStartComparing = true;
211
212 // Update UI to show selected file name
213 const fileInputs = document.querySelectorAll('.file-input');
214 if (fileInputs[viewerId]) {
215 fileInputs[viewerId].placeholder = file.name;
216 }
217
218 } catch (error) {
219 console.error('Error loading file:', error);
220 if (viewerId === 0) {
221 console.log('Error loading file A:', error.message);
222 } else {
223 console.log('Error loading file B:', error.message);
224 }
225 }
226 }
227 };
228
229 input.onerror = (error) => {
230 console.error('File input error:', error);
231 if (viewerId === 0) {
232 console.log('Error selecting file: ' + error.message);
233 } else {
234 console.log('Error selecting file: ' + error.message);
235 }
236 };
237
238 // Trigger the file dialog
239 input.click();
240 }
241}
242
1/* Main layout - side by side containers within #viewer */
2#viewer {
3 display: flex;
4 height: 100%;
5 width: 100%;
6}
7
8/* Side Panel Styles */
9.viewer-wrapper {
10 display: flex;
11 height: 100vh;
12 width: 100%;
13}
14
15.side-panel {
16 width: 300px;
17 min-width: 250px;
18 max-width: 400px;
19 background-color: #f5f5f5;
20 border-right: 1px solid #ddd;
21 box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
22 transition: transform 0.3s ease;
23 z-index: 1000;
24 display: flex;
25 flex-direction: column;
26}
27
28.side-panel.collapsed {
29 transform: translateX(-100%);
30}
31
32.side-panel-header {
33 background-color: #e9ecef;
34 padding: 15px 20px;
35 border-bottom: 1px solid #ddd;
36 flex-shrink: 0;
37}
38
39.side-panel-header h3 {
40 margin: 0;
41 font-size: 18px;
42 font-weight: 600;
43 color: #333;
44}
45
46.side-panel-content {
47 flex: 1;
48 padding: 20px;
49 overflow-y: auto;
50}
51
52.panel-section {
53 margin-bottom: 25px;
54}
55
56.panel-section h4 {
57 margin: 0 0 12px 0;
58 font-size: 14px;
59 font-weight: 600;
60 color: #555;
61 text-transform: uppercase;
62 letter-spacing: 0.5px;
63}
64
65/* File Selection Styles */
66#file-version-a,
67#file-version-b {
68 margin-bottom: 20px;
69 padding: 15px;
70 border: 1px solid #e0e0e0;
71 border-radius: 8px;
72 background-color: #fafafa;
73}
74
75#file-version-a h3,
76#file-version-b h3 {
77 margin: 0 0 10px 0;
78 font-size: 16px;
79 font-weight: 600;
80 color: #333;
81}
82
83.file-input {
84 width: 100%;
85 padding: 8px 12px;
86 margin-bottom: 10px;
87 border: 1px solid #ccc;
88 border-radius: 4px;
89 background-color: #f9f9f9;
90 font-size: 14px;
91 color: #666;
92}
93
94.select-file-btn {
95 background-color: blue;
96 color: white;
97 padding: 8px 16px;
98 border: none;
99 border-radius: 4px;
100 cursor: pointer;
101 font-size: 14px;
102 transition: background-color 0.3s ease;
103}
104
105.select-file-btn:hover {
106 background-color: #0056b3;
107}
108
109/* Sync Toggle Styles */
110#sync-toggle {
111 margin-top: 20px;
112 padding: 15px;
113 border: 1px solid #e0e0e0;
114 border-radius: 8px;
115 background-color: #fafafa;
116}
117
118#sync-slider {
119 width: 60px;
120 height: 30px;
121 -webkit-appearance: none;
122 appearance: none;
123 background-color: #dadee2;
124 cursor: pointer;
125 border-radius: 20px;
126}
127
128/* Slider Track */
129#sync-slider::-webkit-slider-track {
130 background: #1505ee;
131 height: 8px;
132 border-radius: 5px;
133}
134
135#sync-slider::-moz-range-track {
136 background: #0a3ff0;
137 height: 8px;
138 border-radius: 5px;
139 border: none;
140}
141
142/* Slider Thumb/Button */
143#sync-slider::-webkit-slider-thumb {
144 -webkit-appearance: none;
145 appearance: none;
146 height: 30px;
147 width: 30px;
148 border-radius: 50%;
149 background: #007bff;
150 cursor: pointer;
151 border: 2px solid #fff;
152 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
153}
154
155#sync-slider::-moz-range-thumb {
156 height: 24px;
157 width: 24px;
158 border-radius: 50%;
159 background: #007bff;
160 cursor: pointer;
161 border: 2px solid #fff;
162 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
163}
164
165#sync-toggle label {
166 font-size: 14px;
167 font-weight: 500;
168 color: #333;
169}
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales