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
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
6// Global variables to track state
7const defaultDoc1 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/demo-annotated.pdf';
8const defaultDoc2 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf';
9let syncScroll = false;
10let isUpdatingSync = false; // Guard flag to prevent circular calls
11let file1 = null;
12let file2 = null;
13let title1 = '';
14let title2 = '';
15let canStartComparing = false;
16
17// Store references for cleanup
18let documentViewer1 = null;
19let documentViewer2 = null;
20let multiViewerReadyHandler = null;
21let documentLoadedHandler1 = null;
22let documentLoadedHandler2 = null;
23
24// Function to initialize and load the Redaction Tool
25function initializeWebViewer() {
26
27 const element = document.getElementById('viewer');
28 if (!element) {
29 console.error('Viewer div not found.');
30 return;
31 }
32
33 WebViewer({
34 path: '/lib',
35 licenseKey: 'YOUR_LICENSE_KEY',
36 }, element).then(instance => {
37 // Get Core and UI from the instance
38 const { Core, UI } = instance;
39
40 createUIElements();
41 UI.enableElements(['contentEditButton']);
42 UI.disableElements(['comparisonToggleButton']);
43 UI.enableFeatures([UI.Feature.MultiViewerMode]);
44 UI.enterMultiViewerMode();
45 // Create the multi-viewer ready handler
46 multiViewerReadyHandler = () => {
47 // Now both document viewers are available
48 documentViewer1 = Core.getDocumentViewers()[0];
49 documentViewer2 = Core.getDocumentViewers()[1];
50
51 // Create document loaded handlers
52 documentLoadedHandler1 = () => {
53 file1 = documentViewer1.getDocument();
54 };
55
56 documentLoadedHandler2 = () => {
57 file2 = documentViewer2.getDocument();
58 };
59
60 // Add document loaded event listeners
61 documentViewer1.addEventListener('documentLoaded', documentLoadedHandler1);
62 documentViewer2.addEventListener('documentLoaded', documentLoadedHandler2);
63
64 // Load default documents into both viewers
65 documentViewer1?.loadDocument(defaultDoc1);
66 documentViewer2?.loadDocument(defaultDoc2);
67
68 // Add event listeners to sync scroll & zoom button in both viewers
69 addSyncListener(documentViewer1, UI, syncScroll);
70 addSyncListener(documentViewer2, UI, syncScroll);
71 };
72
73 // Set up multi-viewer ready event
74 UI.addEventListener(UI.Events.MULTI_VIEWER_READY, multiViewerReadyHandler);
75
76 }).catch(error => {
77 console.error('Error initializing WebViewer:', error);
78 });
79}
80
81// Add event listener to sync scroll & zoom button in multi-viewer mode
82function addSyncListener(
83 documentViewer,
84 UI,
85 syncScroll
86) {
87 documentViewer
88 ?.getViewerElement()
89 ?.closest('.CompareContainer')
90 // The first button is the "Start Sync" button
91 ?.querySelector('.control-buttons button')
92 ?.addEventListener('click', () => {
93 setTimeout(() => {
94 syncScroll = UI.isMultiViewerSyncing();
95 setSyncScroll(syncScroll);
96 isUpdatingSync = true;
97 UIElements.updateSyncToggleUIOnly(syncScroll);
98 }, 0);
99 });
100};
101
102// Function to sync up document viewers to scroll and zoom together
103function setSyncScroll(value) {
104 syncScroll = value;
105 toggleSyncScrollZoom(syncScroll);
106}
107
108// Function to enable/disable sync scroll & zoom
109function toggleSyncScrollZoom(enable) {
110 if (enable) {
111 window.WebViewer.getInstance().UI.enableMultiViewerSync();
112 console.log('Sync scroll & zoom enabled');
113 syncScroll = true;
114 } else {
115 window.WebViewer.getInstance().UI.disableMultiViewerSync();
116 console.log('Sync scroll & zoom disabled');
117 syncScroll = false;
118 }
119}
120
121// Expose file variables and functions to the global scope for UI interaction
122window.toggleSyncScrollZoom = toggleSyncScrollZoom;
123window.isUpdatingSync = isUpdatingSync;
124window.file1 = file1;
125window.file2 = file2;
126window.title1 = title1;
127window.title2 = title2;
128window.canStartComparing = canStartComparing;
129
130// UI Elements
131// Function to create and initialize UI elements
132function createUIElements() {
133 // Create a container for all controls (label, dropdown, and buttons)
134 // Dynamically load ui-elements.js if not already loaded
135 if (!window.SidePanel) {
136 const script = document.createElement('script');
137 script.src = '/showcase-demos/side-by-side/ui-elements.js';
138 script.onload = () => {
139 UIElements.init('viewer');
140 };
141 document.head.appendChild(script);
142 }
143}
144
145// Initialize the WebViewer
146initializeWebViewer();
147
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