---
title: "JavaScript flipbook PDF viewer | Nutrient"
canonical_url: "https://www.nutrient.io/guides/web/samples/flipbook/"
md_url: "https://www.nutrient.io/guides/web/samples/flipbook.md"
last_updated: "2026-06-09T10:38:40.977Z"
description: "Add a page flip effect to let users browse a PDF like a real magazine. Get additional resources by visiting our JavaScript PDF viewer library."
---

# Flipbook PDF viewer using JavaScript

Add a page flip effect to let users browse a PDF like a real magazine. Get additional resources by visiting our [JavaScript PDF viewer library](/guides/web/viewer.md).

[Get Started](https://www.nutrient.io/sdk/web/getting-started.md)

[All Samples](https://www.nutrient.io/guides/web/samples.md)

[Download](https://www.nutrient.io/guides/web/downloads.md)

[Launch Demo](https://www.nutrient.io/demo/)

---

```js

import PSPDFKit from "@nutrient-sdk/viewer";
import React from "react";
import $ from "jquery";

export async function load(defaultConfiguration) {
  const [instance] = await Promise.all([
    PSPDFKit.load({...defaultConfiguration,
      headless: true,
    }),
    loadTurnJs(),
  ]);

  const { totalPageCount } = instance;
  const magazine = document.getElementById("magazine");

  // set magazine dimensions
  const { width: pageWidth, height: pageHeight } = instance.pageInfoForIndex(0);
  const ratio = pageHeight / pageWidth;
  const magazineWrapper = document.getElementById("magazineWrapper");
  const { clientWidth, clientHeight } = magazineWrapper;
  // To keep the distance from magazine to controls, we need to include
  // wrapperPadding in the calculation of the magazine's dimensions.
  const wrapperPadding = parseInt(
    window.getComputedStyle(magazineWrapper, null).getPropertyValue("padding-bottom")
  );

  const wrapperRatio = clientHeight / clientWidth;

  const isDouble = ratio > wrapperRatio;
  const scaleFactor = isDouble? 0.5 : 1;

  // Define maximum dimensions so controls still fit
  const minMargin = 10;
  const controlsHeight = $("#controls").height();

  const maxWidth =
    (clientHeight - controlsHeight - minMargin - wrapperPadding) /
    ratio /
    scaleFactor;
  const padding = 0.03; // percent
  const magazineWidth = Math.min(clientWidth * (1 - padding), maxWidth);
  const magazineHeight = magazineWidth * ratio * scaleFactor;

  magazine.style.width = `${magazineWidth}px`;
  magazine.style.height = `${magazineHeight}px`;

  // For each page we create a canvas element, render the page's content into it
  // and append it to our magazine container.
  for (let pageIndex = 0; pageIndex < totalPageCount; pageIndex++) {
    const { width, height } = instance.pageInfoForIndex(pageIndex);

    const canvas = document.createElement("canvas");

    canvas.width = pageWidth * window.devicePixelRatio;
    canvas.height = Math.round((canvas.width * height) / width);

    magazine.appendChild(canvas);

    instance.renderPageAsArrayBuffer({ width: canvas.width }, pageIndex).then((buffer) => loadBufferIntoCanvas(buffer, canvas));
  }

  // Turn the magazin container into a browsable magazine
  $("#loading").hide();

  $("#totalPages").text(totalPageCount);

  $(magazine).turn({ display: isDouble? "double" : "single" }).fadeIn().bind("turn", (event, page) => $("#currentPage").text(page));

  $("#controls").fadeIn();

  return instance;
}

// This will asynchronously import turn.js so that it is only loaded when this
// catalog example is opened.
async function loadTurnJs() {
  if (typeof window === "undefined") {
    return;
  }

  window.jQuery = $;

  // The turn.js version we're using is having an issue when detecting to touch
  // event support. To fix this we will temporarily remove the touch constructor
  // while the script is loading.
  const originalTouch = window.Touch;

  try {
    const isTouchDevice =
      "ontouchstart" in window || navigator.msMaxTouchPoints;

    if (!isTouchDevice) {
      delete window.Touch;
    }

    await import("./static/turn.js");
  } finally {
    window.Touch = originalTouch;
  }
}

function loadBufferIntoCanvas(buffer, canvas) {
  const imageView = new Uint8Array(buffer);
  const ctx = canvas.getContext("2d");

  const imageData = ctx.createImageData(canvas.width, canvas.height);

  imageData.data.set(imageView);
  ctx.putImageData(imageData, 0, 0);
}

// By exporting a CustomContainer, we can customize the HTML structure that is
// used by the catalog app.
// We do this so that we can provide the magazine container and custom styles.
export const CustomContainer = React.forwardRef(() => (
  <>
    <div id="magazineWrapper">
      <div id="loading">
        <div />
        <div />
        <div />
        <div />
      </div>
      <div id="magazine" />
      <div id="controls">
        <button onClick={() => $("#magazine").turn("previous")}>

          Previous
        </button>
        <span id="pageIndicator">
          Page&nbsp;
          <span id="currentPage">1</span>
          &nbsp;of&nbsp;
          <span id="totalPages">1</span>
        </span>
        <button onClick={() => $("#magazine").turn("next")}>Next</button>

      </div>
    </div>
    <style jsx>{`
      #magazineWrapper {

        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        height: 100%;
        background: #edf0f4;

        padding-bottom: 30px;
      }

      #magazine {

        display: none;
        margin-bottom: 50px;
      }

      #magazine canvas {

        transform: scale(0.5);
      }

      #pageIndicator {

        color: #999;

        padding: 10px 15px;
      }

      #controls {

        margin-top: 1.5rem;
        background: black;
        opacity: 0.8;
        border-radius: 5px;
        display: none;
        position: absolute;
        z-index: 10000;
        bottom: 15px;
        left: 50%;
        transform: translateX(-50%);
      }

      #controls button {

        background: black;
        color: white;
        margin: 0.5rem;
      }

      #loading {

        display: inline-block;
        position: relative;
        width: 64px;
        height: 64px;
      }

      #loading div {

        position: absolute;
        top: 27px;
        width: 11px;
        height: 11px;
        border-radius: 50%;
        background: rgba(0, 0, 0, 0.1);
        animation-timing-function: cubic-bezier(0, 1, 1, 0);
      }

      #loading div:nth-child(1) {

        left: 6px;
        animation: loading1 0.6s infinite;
      }

      #loading div:nth-child(2) {

        left: 6px;
        animation: loading2 0.6s infinite;
      }

      #loading div:nth-child(3) {

        left: 26px;
        animation: loading2 0.6s infinite;
      }

      #loading div:nth-child(4) {

        left: 45px;
        animation: loading3 0.6s infinite;
      }

      @keyframes loading1 {
        0% {
          transform: scale(0);
        }
        100% {
          transform: scale(1);
        }
      }

      @keyframes loading3 {
        0% {
          transform: scale(1);
        }
        100% {
          transform: scale(0);
        }
      }

      @keyframes loading2 {
        0% {
          transform: translate(0, 0);
        }
        100% {
          transform: translate(19px, 0);
        }
      }
    `}</style>
    <style jsx global>{`.turn-page-wrapper canvas {
        box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.1);
      }

      // Make turn.js not display transparency between spreads.p-temporal {
        background-color: white;
      }
    `}</style>
  </>
));

```

This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.

## FAQ

#### How do I create a flipbook using Nutrient Web SDK?

To create a flipbook, use the Nutrient API to load your PDF document and enable the flipbook mode, which provides a page-flipping animation effect for a realistic reading experience.

#### What are the benefits of using flipbooks in web applications?

Flipbooks offer an engaging way to present documents, allowing users to interact with content in a familiar, book-like format that enhances the reading experience.

#### Can I customize the appearance of a flipbook created with Nutrient?

Yes, you can customize the flipbook’s appearance by modifying CSS styles and using the Nutrient API to adjust settings like page transitions and display options.

#### How do I optimize large PDFs for use in a flipbook?

Optimize large PDFs by compressing images, reducing file size, and using Nutrient’s lazy loading features to improve performance and load times in the flipbook.

#### What are the common challenges in implementing flipbooks with Nutrient?

Challenges include handling large documents, ensuring cross-browser compatibility, and providing smooth animations. Address these by optimizing content and using Nutrient’s performance settings.

---

## Related pages

- [Open, view, and annotate on images using JavaScript](/guides/web/samples/annotating-images.md)
- [Add watermarks to PDFs using JavaScript example](/guides/web/samples/add-watermarks-to-pdf-javascript.md)
- [Add electronic signature images to PDFs using JavaScript](/guides/web/samples/adding-image-electronic-signatures.md)
- [Customize PDF annotation permissions using JavaScript](/guides/web/samples/custom-annotation-permissions.md)
- [Custom HTML PDF annotations using JavaScript](/guides/web/samples/custom-annotations.md)
- [Customize the PDF toolbar using JavaScript](/guides/web/samples/customized-pdf-toolbar.md)
- [Customized Document Editor Toolbar](/guides/web/samples/customized-document-editor-toolbar.md)
- [Edit PDFs using JavaScript](/guides/web/samples/edit-pdf-javascript.md)
- [Add electronic signatures to PDFs using JavaScript](/guides/web/samples/electronic-signatures-in-pdf.md)
- [Customizing PDF text search using JavaScript](/guides/web/samples/customized-pdf-search.md)
- [View PDFs in dark mode using JavaScript](/guides/web/samples/dark-mode-pdf-viewer.md)
- [Create custom overlays on PDFs using JavaScript](/guides/web/samples/custom-overlay-items.md)
- [Disable PDF editing and annotations](/guides/web/samples/open-read-only-pdf.md)
- [PDF form support using JavaScript](/guides/web/samples/javascript-pdf-form.md)
- [Customize PDF annotation tooltips using JavaScript](/guides/web/samples/custom-annotation-tooltip.md)
- [Customizing JavaScript PDF printing modes](/guides/web/samples/pdf-printing-modes.md)
- [Open PDFs using JavaScript](/guides/web/samples/open-pdf-using-javascript.md)
- [PDF text selection using JavaScript](/guides/web/samples/pdf-text-selection-javascript.md)
- [Handling password-protected PDFs in our JavaScript viewer](/guides/web/samples/password-protected-pdf.md)
- [Zoom example for our JavaScript PDF viewer](/guides/web/samples/zooming.md)
- [Collaborate on PDFs using JavaScript](/guides/web/samples/instant-pdf-collaboration.md)
- [JavaScript PDF magazine viewer](/guides/web/samples/javascript-magazine-viewer.md)
- [Hide or reveal area on PDFs using JavaScript](/guides/web/samples/hide-reveal-area-in-pdf.md)
- [Digitally sign a PDF using JavaScript](/guides/web/samples/javascript-digital-signatures.md)
- [PDF annotation in JavaScript](/guides/web/samples/javascript-pdf-annotations.md)
- [PDF Collaboration permissions using JavaScript](/guides/web/samples/collaboration-permissions.md)
- [Drag-and-drop UI in our JavaScript PDF viewer](/guides/web/samples/drag-and-drop.md)
- [Redact PDFs using JavaScript](/guides/web/samples/javascript-pdf-redaction.md)
- [Storing electronic signatures in the browser using JavaScript](/guides/web/samples/stored-electronic-signatures.md)
- [PDF presentation mode using JavaScript](/guides/web/samples/presentation-mode.md)
- [Customize the UI for PDF annotations using JavaScript](/guides/web/samples/annotations-inspector.md)

