> 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-online-flipbook.md).

# Online Flipbook Showcase Demo Code Sample

Add page-turning flipbook animation experience to online content

{% 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/online-flipbook" class="button primary">Live demo</a>
{% endhint %}

Easily enhance your online content with an interactive, page-turning flipbook experience.

This demo allows you to:

* Choose your own PDF file.
* Add flipbook animation to online content.

**Implementation steps**

To add online flipbook 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.

**Requires**

* JQuery 1.3 or above
* [turn.js plugin](http://www.turnjs.com/)

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
// Copilot name: GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-10-16
// File: showcase-demos/online-flipbook/index.js

// Setting the path to the Web Worker JS file relative to the html file of the viewer
Core.setWorkerPath('../../lib/core');
//Set license key, source, and viewer element
const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';
const source = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf';
const viewer = document.getElementById('viewer');

// Create flipbook container
const flipbook = document.createElement('div');
flipbook.id = 'flipbook';

// Loading message
const loadingMessage = document.createElement('div');
loadingMessage.id = 'loading-message';
loadingMessage.style.position = 'absolute';
loadingMessage.innerHTML = 'Preparing document...';

flipbook.appendChild(loadingMessage);
viewer.appendChild(flipbook);

// Create navigation buttons for previous and next page
const previousButton = document.createElement('button');
previousButton.className = 'btn-style';
previousButton.id = 'previous';
previousButton.innerHTML = 'Previous Page';
previousButton.onclick = () => $('#flipbook').turn('previous');

const nextButton = document.createElement('button');
nextButton.className = 'btn-style';
nextButton.id = 'next';
nextButton.innerHTML = 'Next Page';
nextButton.onclick = () => $('#flipbook').turn('next');

// Container for buttons (file picker will be added after DocumentViewer is created)
const controlsContainer = document.createElement('div');
controlsContainer.className = 'button-container';
controlsContainer.appendChild(previousButton);
controlsContainer.appendChild(nextButton);
viewer.insertBefore(controlsContainer, viewer.firstChild);

// Create an instance of the DocumentViewer
const documentViewer = new Core.DocumentViewer();

// Set the viewer and scrollview elements that DocumentViewer will append rendered pages to.
const viewerElement = document.createElement('div');
documentViewer.setViewerElement(viewerElement);
documentViewer.setScrollViewElement(viewerElement);
documentViewer.loadAsPDF = true;

// Load the document with explicit extension and error handling
try {
  documentViewer.loadDocument(source, { extension: 'pdf' });
} catch (error) {
  console.error('Error loading default document:', error);
  loadingMessage.innerHTML = `Error loading default document: ${error.message || 'Network failure'}. Please use the file picker to load a local document.`;
  loadingMessage.style.color = 'red';
}

// Add error event listener for network failures
documentViewer.addEventListener('documentLoadingProgress', (progress) => {
  if (loadingMessage &#x26;&#x26; flipbook.contains(loadingMessage)) {
    loadingMessage.innerHTML = `Loading document... ${Math.round(progress * 100)}%`;
  }
});

documentViewer.addEventListener('loaderror', (error) => {
  console.error('Document load error:', error);
  if (loadingMessage) {
    loadingMessage.innerHTML = `Failed to load document: ${error.message || 'Network failure'}. Please check your connection and try again.`;
    loadingMessage.style.color = 'red';
  }
});

// Event listener for when the document is fully loaded
documentViewer.addEventListener('documentLoaded', () => {
  // Complete reset of flipbook container to virgin state
  const $flipbook = $('#flipbook');

  // Remove all turn.js data attributes
  $flipbook.removeData(); // Clears all jQuery data

  // Remove all turn.js CSS classes
  $flipbook.removeClass(); // Remove all classes

  // Remove all turn.js HTML attributes
  const turnAttributes = [
    'data-turn', 'data-turn-page', 'data-turn-pages', 'data-turn-size',
    'data-turn-orig-width', 'data-turn-orig-height', 'data-turn-page-number',
    'data-turn-direction', 'data-turn-when', 'data-turn-corners'
  ];
  turnAttributes.forEach(attr => $flipbook[0].removeAttribute(attr));

  // Reset inline styles to original state
  $flipbook[0].style.cssText = 'transition: margin-left 0.2s ease-in-out; margin: 0px auto; width: 90%; height: 90%;';

  // Clear all existing content from flipbook container
  while (flipbook.firstChild) {
    flipbook.removeChild(flipbook.firstChild);
  }

  // Re-add loading message
  loadingMessage.innerHTML = 'Preparing document...';
  loadingMessage.style.color = '';
  flipbook.appendChild(loadingMessage);
  console.log('Added loading message');
  const doc = documentViewer.getDocument();
  const info = doc.getPageInfo(1);
  const width = info.width;
  const height = info.height;
  const pageCount = doc.getPageCount();
  const promises = [];
  const canvases = [];
  const boundingRect = flipbook.getBoundingClientRect();

  // Calculate flipbook dimensions based on page aspect ratio
  let flipbookHeight = boundingRect.height;
  let flipbookWidth = boundingRect.width;
  if (((flipbookHeight * width) / height) * 2 &#x3C; flipbookWidth)
    flipbookWidth = ((flipbookHeight * width) / height) * 2;
  else
    flipbookHeight = ((flipbookWidth / width) * height) / 2;

  // Load each page's canvas
  for (let i = 0; i &#x3C; pageCount; i++) {
    const pageNumber = i + 1;
    promises.push(
      doc.requirePage(pageNumber).then(() => {
        return new Promise(resolve => {
          doc.loadCanvas({
            pageNumber,
            drawComplete: (canvas, index) => {
              canvases.push({ index, canvas });
              loadingMessage.innerHTML = `Loading page canvas... (${canvases.length}/${pageCount})`;
              resolve();
            },
          });
        });
      })
    );
  }

  // Once all pages are loaded, initialize the flipbook
  Promise.all(promises).then(() => {
    // Safely remove loading message if it exists as a child
    if (flipbook.contains(loadingMessage)) {
      flipbook.removeChild(loadingMessage);
    } else {
      console.log('Loading message was already removed or not found in flipbook');
    }

    if (canvases.length === 1) {
      // Handle single-page documents without turn.js to avoid range errors
      console.log('Single page document - displaying without flipbook functionality');
      const canvas = canvases[0].canvas;
      flipbook.appendChild(canvas);

      // Hide navigation buttons for single page
      console.log('Hiding buttons for single page document');
      if (previousButton) {
        previousButton.style.display = 'none';
        console.log('Previous button hidden');
      }
      if (nextButton) {
        nextButton.style.display = 'none';
        console.log('Next button hidden');
      }

    } else {
      // Handle multi-page documents with turn.js flipbook
      // Wrap each canvas in a div for proper turn.js page structure
      canvases.sort((a, b) => a.index - b.index).forEach((o, index) => {
        const pageDiv = document.createElement('div');
        // Add page number indicator
        const pageNumber = document.createElement('div');
        pageNumber.className = 'page-number';
        pageNumber.textContent = `${index + 1}`;
        pageDiv.appendChild(pageNumber);
        pageDiv.appendChild(o.canvas);
        flipbook.appendChild(pageDiv);
      });

      // Initialize turn.js 
      $('#flipbook').turn({
        width: flipbookWidth,
        height: flipbookHeight,
        autoCenter: true,
        pages: canvases.length,
        page: 1,
        elevation: 50,
        gradients: true,
        acceleration: true,
        duration: 400,
        shadows: true,
        shadowBlur: 6,
      });

      // Show navigation buttons for multi-page
      console.log('Showing buttons for multi-page document');
      if (previousButton) {
        previousButton.style.display = 'inline-block';
        console.log('Previous button shown');
      }
      if (nextButton) {
        nextButton.style.display = 'inline-block';
        console.log('Next button shown');
      }
      // Automatically advance to page 2 to demonstrate flipbook
      setTimeout(() => {
        try {
          $('#flipbook').turn('next');
        } catch (error) {
          console.error('Error navigating to next page:', error);
        }
      }, 500);
    }
  }).catch(error => {
    console.error('Error loading flipbook pages:', error);

    // Provide specific error messages based on error type
    let errorMessage = 'Error loading pages. ';
    if (error.message &#x26;&#x26; error.message.includes('Network failure')) {
      errorMessage += 'Network connection issue. Please check your internet connection and try again.';
    } else if (error.message &#x26;&#x26; error.message.includes('CORS')) {
      errorMessage += 'File access issue. Try using a local file instead.';
    } else {
      errorMessage += `${error.message || 'Unknown error'}. Please try again with a different file.`;
    }
    loadingMessage.innerHTML = errorMessage;
    loadingMessage.style.color = 'red';
  });
});

// UI Elements
// ui-elements.js
// Function to create and initialize UI elements
function createUIElements() {
  return new Promise((resolve, reject) => {
    // Dynamically load ui-elements.js if not already loaded
    if (window.UIElements) {
      console.log('UIElements already loaded');
      resolve();
      return;
    }
    const script = document.createElement('script');
    script.src = '/showcase-demos/online-flipbook/ui-elements.js';
    script.onload = () => {
      console.log('✅ UIElements script loaded successfully');
      UIElements.init();
      resolve();
    };
    script.onerror = () => {
      console.error('Failed to load UIElements script');
      reject(new Error('Failed to load ui-elements.js'));
    };
    document.head.appendChild(script);
  });
}

// Create file picker button using UIElements class (async)
createUIElements().then(() => {
  console.log('UIElements loaded successfully');
  const filePickerButton = UIElements.createFilePickerButton(documentViewer, loadingMessage);
  controlsContainer.appendChild(filePickerButton); // Add Pick File button to the right of Next Page
}).catch(error => {
  console.error('Failed to load UIElements:', error);
});

</code></pre>

{% endtab %}

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

```js
// ES6 Compliant Syntax
// File: showcase-demos/online-flipbook/ui-elements.js

class UIElements {

    static init() {
        console.log(`UIElements initialized}`);
       
    }

    static createFilePickerButton(documentViewer, loadingMessage) {
        const filePickerButton = document.createElement('button');
        filePickerButton.className = 'btn-style';
        filePickerButton.id = 'file-picker';
        filePickerButton.innerHTML = 'Pick File';
        filePickerButton.onclick = () => {
            // Open file picker dialog
            const fileInput = document.createElement('input');
            fileInput.type = 'file';
            fileInput.accept = '.pdf,.doc,.docx,.ppt,.pptx';// Accept Office file formats
            fileInput.onchange = async (event) => {
                const file = event.target.files[0];
                if (file) {
                    try {
                        const url = URL.createObjectURL(file);
                        
                        // Extract file extension from filename
                        const fileName = file.name;
                        const extension = fileName.split('.').pop().toLowerCase();
                        
                        console.log(`Loading file: ${file.name} (extension: ${extension}) blob URL: ${url}`);
                        
                        // Add loading feedback
                        loadingMessage.innerHTML = `Loading ${fileName}...`;
                        loadingMessage.style.color = '#666';
                        
                        // Load document with explicit extension and error handling
                        await documentViewer.loadDocument(url, { 
                            extension: extension,
                            // Add additional options for better compatibility
                            withCredentials: false,
                            customHeaders: {}
                        });
                        
                    } catch (error) {
                        console.error('Error loading file:', error);
                        loadingMessage.innerHTML = `Error loading file: ${error.message || 'Network failure'}. Please try again.`;
                        loadingMessage.style.color = 'red';
                        
                        // Clean up blob URL on error
                        if (url) {
                            URL.revokeObjectURL(url);
                        }
                    }
                }
            };
            fileInput.click();
        };
        
        return filePickerButton;
    }
}
```

{% endcode %}
{% endtab %}

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

```css
/* Base Button Styles */
.btn-style {
    margin: 10px;
    padding: 5px 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: bold;
    transition: all 0.2s ease;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    color: white;
}

/* Load Button Specific Styles */
.btn-style {
    background-color: #007bff;
}

.btn-style:hover {
    background-color: #0056b3;
    transform: translateY(-1px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}

.btn-style:active {
    transform: translateY(1px);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

/* Button Container */
.button-container {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 15px;
    margin: 5px 0;
    padding-bottom: 5px;
    border-bottom: 1px solid #e0e0e0;
    background-color: rgba(112, 198, 255, 0.2
            /* Light blue background for contrast */
        );
}

/* Responsive Design */
@media (max-width: 768px) {
    .btn-style {
        margin: 5px;
        padding: 8px 12px;
        font-size: 16px;
    }

    .button-container {
        align-items: center;
    }
}

/* Pick File Button Specific Styles */
#file-picker {
    background-color: #28a745; /* Green color to distinguish from navigation buttons */
    border-color: #1e7e34;
}

#file-picker:hover {
    background-color: #1e7e34;
    border-color: #155724;
}

#file-picker:active {
    background-color: #155724;
}

/* Navigation Button Spacing */
#previous, #next {
    background-color: #007bff; /* Keep blue for navigation */
}

/* Enhanced Button Container Layout */
.button-container {
    justify-content: flex-start; /* Align buttons to the left */
    padding: 10px 15px;
    background: linear-gradient(135deg, rgba(112, 198, 255, 0.2), rgba(112, 198, 255, 0.1));
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Flipbook Container - minimal styling to avoid turn.js conflicts */
#flipbook {
    margin: 20px auto;
}

/* Turn.js Page Styles - let turn.js handle all visual effects */
.turn-page {
    background-color: #ffffff;
}

/* Remove all custom hover and animation effects - let turn.js handle everything */

/* Remove all hard page and shadow overrides - let turn.js handle these */

/* Removed hover effects to prevent interference with turn.js */

/* Loading state styling */
#loading-message {
    text-align: center;
    font-size: 16px;
    color: #666;
    padding: 40px;
    background-color: #f8f9fa;
    border-radius: 4px;
    margin: 20px;
}

/* Page number indicator (optional) */
.page-number {
    position: absolute;
    bottom: 10px;
    right: 15px;
    font-size: 12px;
    color: #888;
    background: rgba(255, 255, 255, 0.8);
    padding: 2px 6px;
    border-radius: 3px;
    z-index: 10;
}

/* Canvas styling within pages */
canvas {
    width: 100% !important;
    height: 100% !important;
    object-fit: contain;
    background-color: white;
}

/* Remove blank page styling - let turn.js handle all page states */

/* Simplified page styling - removed transform conflicts */

/* Remove all turn.js interference - let turn.js handle all visual effects */

/* Removed custom turn animation to prevent conflicts with turn.js */

/* Mobile responsiveness for flipbook */
@media (max-width: 768px) {
    #flipbook {
        margin: 10px auto;
    }
    
    .turn-page {
        border-width: 1px;
    }
}
```

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

[View the full sample on GitHub](https://github.com/ApryseSDK/webviewer-samples/tree/main/showcase-demos-playground)


---

# 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-online-flipbook.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.
