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
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