> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-side-by-side.md).

# Side-by-Side Showcase Demo Code Sample

View and compare PDFs, Office documents, and images with a side-by-side layout.

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://showcase.apryse.com/side-by-side" class="button primary">Live demo</a>
{% endhint %}

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:

* Upload and choose your own documents.
* View two files side-by-side.
* Toggle synchronized scrolling and zooming between documents.

**Implementation steps**

To add Side-by-Side viewing capability with WebViewer: Step 1: [Get started with WebViewer](/web/get-started/readme.md) 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-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

{% tabs %}
{% tab title="index.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// ES6 Compliant Syntax
// GitHub Copilot, Claude Sonnet 4 (Preview), October 16, 2025
// File: showcase-demos/side-by-side/index.js
import WebViewer from '@pdftron/webviewer';

const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';

// Global variables to track state
const defaultDoc1 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/demo-annotated.pdf';
const defaultDoc2 = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf';
let syncScroll = false;
let isUpdatingSync = false; // Guard flag to prevent circular calls
let file1 = null;
let file2 = null;
let title1 = '';
let title2 = '';
let canStartComparing = false;

// Store references for cleanup
let documentViewer1 = null;
let documentViewer2 = null;
let multiViewerReadyHandler = null;
let documentLoadedHandler1 = null;
let documentLoadedHandler2 = null;

// Function to initialize and load the Redaction Tool
function initializeWebViewer() {

  const element = document.getElementById('viewer');
  if (!element) {
    console.error('Viewer div not found.');
    return;
  }

  WebViewer({
    path: '/lib',
    licenseKey: licenseKey,
  }, element).then(instance => {    
    // Get Core and UI from the instance
    const { Core, UI } = instance;

    createUIElements();
    UI.enableElements(['contentEditButton']);
    UI.disableElements(['comparisonToggleButton']);
    UI.enableFeatures([UI.Feature.MultiViewerMode]);
    UI.enterMultiViewerMode();
    // Create the multi-viewer ready handler
    multiViewerReadyHandler = () => {
      // Now both document viewers are available
      documentViewer1 = Core.getDocumentViewers()[0];
      documentViewer2 = Core.getDocumentViewers()[1];

      // Create document loaded handlers
      documentLoadedHandler1 = () => {
        file1 = documentViewer1.getDocument();
      };

      documentLoadedHandler2 = () => {
        file2 = documentViewer2.getDocument();
      };

      // Add document loaded event listeners
      documentViewer1.addEventListener('documentLoaded', documentLoadedHandler1);
      documentViewer2.addEventListener('documentLoaded', documentLoadedHandler2);

      // Load default documents into both viewers
      documentViewer1?.loadDocument(defaultDoc1);
      documentViewer2?.loadDocument(defaultDoc2);
      
      // Add event listeners to sync scroll &#x26; zoom button in both viewers
      addSyncListener(documentViewer1, UI, syncScroll);
      addSyncListener(documentViewer2, UI, syncScroll);
    };

    // Set up multi-viewer ready event
    UI.addEventListener(UI.Events.MULTI_VIEWER_READY, multiViewerReadyHandler);

  }).catch(error => {
    console.error('Error initializing WebViewer:', error);
  });
}

// Add event listener to sync scroll &#x26; zoom button in multi-viewer mode
function addSyncListener(
  documentViewer,
  UI,
  syncScroll
) {
  documentViewer
    ?.getViewerElement()
    ?.closest('.CompareContainer')
    // The first button is the "Start Sync" button
    ?.querySelector('.control-buttons button')
    ?.addEventListener('click', () => {
      setTimeout(() => {
        syncScroll = UI.isMultiViewerSyncing();
        setSyncScroll(syncScroll);
        isUpdatingSync = true;
        UIElements.updateSyncToggleUIOnly(syncScroll);
      }, 0);
    });
};

// Function to sync up document viewers to scroll and zoom together
function setSyncScroll(value) {
  syncScroll = value;
  toggleSyncScrollZoom(syncScroll);
}

// Function to enable/disable sync scroll &#x26; zoom
function toggleSyncScrollZoom(enable) {
  if (enable) {
    window.WebViewer.getInstance().UI.enableMultiViewerSync();
    console.log('Sync scroll &#x26; zoom enabled');
    syncScroll = true;
  } else {
    window.WebViewer.getInstance().UI.disableMultiViewerSync();
    console.log('Sync scroll &#x26; zoom disabled');
    syncScroll = false;  
  }  
}

// Expose file variables and functions to the global scope for UI interaction
window.toggleSyncScrollZoom = toggleSyncScrollZoom;
window.isUpdatingSync = isUpdatingSync;
window.file1 = file1;
window.file2 = file2;
window.title1 = title1;
window.title2 = title2;
window.canStartComparing = canStartComparing;

// UI Elements
// Function to create and initialize UI elements
function createUIElements() {
  // Create a container for all controls (label, dropdown, and buttons)
  // Dynamically load ui-elements.js if not already loaded
  if (!window.SidePanel) {
    const script = document.createElement('script');
    script.src = '/showcase-demos/side-by-side/ui-elements.js';
    script.onload = () => {
      UIElements.init('viewer');
    };
    document.head.appendChild(script);
  }
}

// Initialize the WebViewer
initializeWebViewer();

</code></pre>

{% endtab %}

{% tab title="ui-elements.js" %}
{% code title="ui-elements.js" lineNumbers="true" %}

```js
// ES6 Compliant Syntax
// GitHub Copilot, Claude Sonnet 4 (Preview), October 16, 2025
// File: showcase-demos/side-by-side/ui-elements.js

class UIElements {

  static init(viewerId) {
    this.createSidePanel(viewerId);
    this.setupEventHandlers();
  }

  // Function to create a side panel that sits on the left side of the viewer
  static createSidePanel(viewerId) {
    const viewerElement = document.getElementById(viewerId);
    if (!viewerElement) {
      console.error(`Viewer element with id '${viewerId}' not found.`);
      return;
    }

    // Create the side panel container
    const sidePanel = document.createElement('div');
    sidePanel.id = 'side-panel';
    sidePanel.className = 'side-panel';

    // Create side panel content
    const content = document.createElement('div');
    content.className = 'side-panel-content';

    // Add the text extraction content
    const sampleContent = document.createElement('div');
    sampleContent.innerHTML = `
      <div class="panel-section">
        <h4>Side by Side Demo Files</h4>
        
        <!-- File Version A (Left) -->
        <div id="file-version-a">
          <h3>File Version A (Left)</h3>
          <input type="text" readonly placeholder="demo-annotated.pdf" class="file-input">
          <button class="select-file-btn" id="btn-file-version-a">Select File</button>
        </div>
        
        <!-- File Version B (Right) -->
        <div id="file-version-b">
          <h3>File Version B (Right)</h3>
          <input type="text" readonly placeholder="WebViewerDemoDoc.pdf" class="file-input">
          <button class="select-file-btn" id="btn-file-version-b">Select File</button>
        </div>
        
        <!-- Sync Toggle -->
        <div id="sync-toggle">
          <input type="range" min="0" max="1" value="0" class="sync-slider" id="sync-slider">
          <br>
          <label for="sync-slider">Sync Scroll & Zoom</label>
        </div>
 
      </div>
    `;

    content.appendChild(sampleContent);
    sidePanel.appendChild(content);

    // Create a wrapper to contain both the side panel and viewer
    const wrapper = document.createElement('div');
    wrapper.id = 'viewer-wrapper';
    wrapper.className = 'viewer-wrapper';

    // Insert the wrapper before the viewer element
    viewerElement.parentNode.insertBefore(wrapper, viewerElement);

    // Move the viewer element into the wrapper and add the side panel
    wrapper.appendChild(sidePanel);
    wrapper.appendChild(viewerElement);

    // Add the viewer-with-panel class to the viewer element
    viewerElement.classList.add('viewer-with-panel');
  }

  // Function to add content to the side panel
  addPanelContent(content) {
    const panelContent = document.querySelector('.side-panel-content');
    if (panelContent) {
      const contentDiv = document.createElement('div');
      contentDiv.className = 'panel-section';
      contentDiv.innerHTML = content;
      panelContent.appendChild(contentDiv);
    }
  }

  static updateSyncToggleUIOnly(syncScroll) {
    window.isUpdatingSync = true;
    const syncSlider = document.getElementById('sync-slider');
    if (syncSlider) {
      // Guard against circular calls
      syncSlider.value = syncScroll ? '1' : '0';
      console.log('Sync slider UI updated to:', syncSlider.value);
            // Function to update slider background color
      const updateSliderBackground = (value) => {
        if (value === '1') {
          // Toggled to the right - light blue background
          syncSlider.style.backgroundColor = '#87CEEB';
        } else {
          // Toggled to the left - gray background
          syncSlider.style.backgroundColor = '#dadee2';
        }
      };
      // Set initial background color
      updateSliderBackground(syncSlider.value);
      toggleSyncScrollZoom(syncScroll);
      console.log('Sync slider background color updated to:', syncSlider.style.backgroundColor);
      console.log('Sync toggle UI only updated to:', syncScroll);
      console.log('syncSlider.value:', syncSlider.value);
    }
  }
  // Setup event handlers for UI elements
  static setupEventHandlers() {
    // Sync toggle slider event listener
    const syncSlider = document.getElementById('sync-slider');
    if (syncSlider) {
      // Function to update slider background color
      const updateSliderBackground = (value) => {
        if (value === '1') {
          // Toggled to the right - light blue background
          syncSlider.style.backgroundColor = '#87CEEB';
        } else {
          // Toggled to the left - gray background
          syncSlider.style.backgroundColor = '#dadee2';
        }
      };

      // Set initial background color
      updateSliderBackground(syncSlider.value);

      syncSlider.addEventListener('change', (event) => {
        const isSync = event.target.value === '1';
        // Update background color
        updateSliderBackground(event.target.value);

        // Guard against circular calls
        if (window.isUpdatingSync) {
          window.isUpdatingSync = false;
        }

        // Call the toggleSyncScrollZoom function in index.js if it exists
        if (window.toggleSyncScrollZoom && typeof window.toggleSyncScrollZoom === 'function') {
          window.toggleSyncScrollZoom(isSync);
        }

      });

      // Also listen for input event for real-time updates while dragging
      syncSlider.addEventListener('input', (event) => {
        updateSliderBackground(event.target.value);
      });
    }

    // File selection button event listeners
    const selectFileButtons = document.querySelectorAll('.select-file-btn');
    selectFileButtons.forEach((button, index) => {
      button.addEventListener('click', () => {

        if (button.id === 'btn-file-version-a') {
          UIElements.filePicker('A');
        }
        else if (button.id === 'btn-file-version-b') {
          UIElements.filePicker('B');
        }
        else if (button.id === 'sync-slider') {
          window.toggleSyncScrollZoom((index === 1 ? true : false));
        }
      });
    });
  }

  // Method to handle file upload for either side
  static filePicker(selectSide) {
    console.log(`Upload file for side: ${selectSide}`);
    
    const instance = window.WebViewer.getInstance();
    const viewerId = selectSide === 'A' ? 0 : 1;

    // Create and trigger file input dialog directly
    const input = document.createElement('input');
    input.type = 'file';
    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';
    
    input.onchange = async (event) => {
      const file = event.target.files[0];
      if (file) {
        try {
          const doc = await instance.Core.createDocument(file);

          if (viewerId === 0) {
            if (window.file1) {
              window.file1.unloadResources();
            }
            window.file1 = doc;
            instance.Core.getDocumentViewers()[0].loadDocument(doc);
            window.title1 = file.name || 'File A';
            console.log('Loaded file A:', window.title1);
          } else {
            if (window.file2) {
              window.file2.unloadResources();
            }
            window.file2 = doc;
            instance.Core.getDocumentViewers()[1].loadDocument(doc);
            window.title2 = file.name || 'File B';
            console.log('Loaded file B:', window.title2);
          }
          
          window.canStartComparing = true;
          
          // Update UI to show selected file name
          const fileInputs = document.querySelectorAll('.file-input');
          if (fileInputs[viewerId]) {
            fileInputs[viewerId].placeholder = file.name;
          }
          
        } catch (error) {
          console.error('Error loading file:', error);
          if (viewerId === 0) {
            console.log('Error loading file A:', error.message);
          } else {
            console.log('Error loading file B:', error.message);
          }
        }
      }
    };
    
    input.onerror = (error) => {
      console.error('File input error:', error);
      if (viewerId === 0) {
        console.log('Error selecting file: ' + error.message);
      } else {
        console.log('Error selecting file: ' + error.message);
      }
    };
    
    // Trigger the file dialog
    input.click();
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="index.css" %}
{% code title="index.css" lineNumbers="true" %}

```css
/* Main layout - side by side containers within #viewer */
#viewer {
  display: flex;
  height: 100%;
  width: 100%;
}

/* Side Panel Styles */
.viewer-wrapper {
  display: flex;
  height: 100vh;
  width: 100%;
}

.side-panel {
  width: 300px;
  min-width: 250px;
  max-width: 400px;
  background-color: #f5f5f5;
  border-right: 1px solid #ddd;
  box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s ease;
  z-index: 1000;
  display: flex;
  flex-direction: column;
}

.side-panel.collapsed {
  transform: translateX(-100%);
}

.side-panel-header {
  background-color: #e9ecef;
  padding: 15px 20px;
  border-bottom: 1px solid #ddd;
  flex-shrink: 0;
}

.side-panel-header h3 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}

.side-panel-content {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}

.panel-section {
  margin-bottom: 25px;
}

.panel-section h4 {
  margin: 0 0 12px 0;
  font-size: 14px;
  font-weight: 600;
  color: #555;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* File Selection Styles */
#file-version-a,
#file-version-b {
  margin-bottom: 20px;
  padding: 15px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  background-color: #fafafa;
}

#file-version-a h3,
#file-version-b h3 {
  margin: 0 0 10px 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.file-input {
  width: 100%;
  padding: 8px 12px;
  margin-bottom: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background-color: #f9f9f9;
  font-size: 14px;
  color: #666;
}

.select-file-btn {
  background-color: blue;
  color: white;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.3s ease;
}

.select-file-btn:hover {
  background-color: #0056b3;
}

/* Sync Toggle Styles */
#sync-toggle {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  background-color: #fafafa;
}

#sync-slider {
  width: 60px;
  height: 30px;
  -webkit-appearance: none;
  appearance: none;
  background-color: #dadee2;
  cursor: pointer;
  border-radius: 20px;
}

/* Slider Track */
#sync-slider::-webkit-slider-track {
  background: #1505ee;
  height: 8px;
  border-radius: 5px;
}

#sync-slider::-moz-range-track {
  background: #0a3ff0;
  height: 8px;
  border-radius: 5px;
  border: none;
}

/* Slider Thumb/Button */
#sync-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  height: 30px;
  width: 30px;
  border-radius: 50%;
  background: #007bff;
  cursor: pointer;
  border: 2px solid #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

#sync-slider::-moz-range-thumb {
  height: 24px;
  width: 24px;
  border-radius: 50%;
  background: #007bff;
  cursor: pointer;
  border: 2px solid #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

#sync-toggle label {
  font-size: 14px;
  font-weight: 500;
  color: #333;
}
```

{% endcode %}
{% endtab %}
{% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-side-by-side.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
