DEV Community

Cover image for Mantine Book — Grab Any Edge, Turn Any Page
Giovambattista Fazioli
Giovambattista Fazioli

Posted on

Mantine Book — Grab Any Edge, Turn Any Page

A realistic iBooks-style book for React built on Mantine: stack two-sided pages and turn them by dragging any point of the free edge — a pure-DOM reflection fold by default, a true 3D WebGL curl when you want it.

Introduction

Most page-flip libraries cheat: you can only grab the corner, and the fold is a special-cased animation that falls apart the moment you drag somewhere unexpected. This component started from the opposite question — what's the general math of a turning page? The answer is a perpendicular-bisector reflection fold: the grabbed point folds exactly onto your pointer, and the crease is computed from that single constraint. One code path covers every grab point and every drag direction — corner, mid-edge, upward, downward, forward, back. The classic corner turn is just the special case.

What is Mantine Book?

@gfazioli/mantine-book renders a realistic, interactive book: a stack of two-sided pages you turn like real paper. Drag the right half to go forward and the left half to go back, or drive it programmatically through a controlled page state — arrows, dots, sliders, keyboard, anything.

Mantine Book

It ships two rendering paths. The default flat variant is a pure DOM + CSS reflection fold — SSR-safe, zero canvas, interactive from the first paint. The opt-in rounded variant draws a true 3D curl on a WebGL canvas, with the page wrapping around the crease and a soft specular ridge along the bend. If WebGL is unavailable or a face snapshot fails, rounded degrades to flat automatically — opting in is always safe.

Any JSX goes inside a page face — text, images, MDX, even live components. Faces are rendered once by React (no innerHTML cloning), so the event handlers inside your pages stay alive.

✨ Key Features

Compound API or data-driven pages

Compose pages declaratively, or hand the book an array — same result:

<Book width={260} height={360}>
  <Book.Page>
    <Book.Page.Front>Front of page 1</Book.Page.Front>
    <Book.Page.Back>Back of page 1</Book.Page.Back>
  </Book.Page>
</Book>
Enter fullscreen mode Exit fullscreen mode
const pages: BookPageData[] = [
  { front: <Cover />, back: <TableOfContents /> },
  { front: <Chapter n={1} />, back: <Chapter n={2} /> },
];

<Book width={260} height={360} pages={pages} />
Enter fullscreen mode Exit fullscreen mode

Grab anywhere, drag anywhere

The fold is a perpendicular-bisector reflection: the crease is derived from the segment between the grab point and the pointer, so the page follows your finger exactly — from any point of the free edge, in any direction. Releases settle naturally: past flipThreshold (or with a quick side-aware swipe) the turn completes; otherwise the page snaps back.

Two renderers: flat and rounded

// Pure DOM reflection fold — the default, SSR-safe
<Book variant="flat" /* ... */ />

// True 3D WebGL curl with a lit specular ridge
<Book variant="rounded" curlRadius={90} /* ... */ />
Enter fullscreen mode Exit fullscreen mode

The WebGL layer is lazy: it loads only when a rounded curl actually mounts, so flat users pay nothing. The whole book shares a single pooled WebGL context, no matter how many pages it has.

Controlled navigation with face-based indices

Page i has its front at face 2i and its back at 2i + 1 — face 0 is the closed book. Programmatic turns animate like a real drag:

const [page, setPage] = useState(0);

<Book page={page} onPageChange={setPage} pages={pages} />
<Button onClick={() => setPage((p) => p + 2)}>Next spread</Button>
Enter fullscreen mode Exit fullscreen mode

Riffle through big jumps

Jumping several pages at once doesn't teleport — the book riffles through every page in between, with the total time budget compressed into riffleDuration (default 1000ms) and turns queued one page in flight at a time. Wire it to a slider and scrub through a 100-page book.

Hard covers

<Book withCover pages={pages} />
Enter fullscreen mode Exit fullscreen mode

With withCover, the first and last pages turn rigid around the spine — no curl, like the board of a hardcover — and the closed book is compact: centered on the play-zone, sliding into the two-page spread as the cover opens (the slide respects prefers-reduced-motion).

Reveal layers

revealBackground on the Book paints the inside cover — visible where no page rests and in the area a turning page uncovers. The same prop on a single Book.Page adds a side-aware layer under that page only.

Accessible by default

Keyboard turns (arrow keys, Home, End), a polite screen-reader live region announcing the visible pages (customizable via pageAnnouncement), and reduced-motion support throughout.

🚀 Getting Started

yarn add @gfazioli/mantine-book
# or
npm install @gfazioli/mantine-book
Enter fullscreen mode Exit fullscreen mode
import '@gfazioli/mantine-book/styles.css';
import { Book } from '@gfazioli/mantine-book';

export function Demo() {
  return (
    <Book width={260} height={360}>
      <Book.Page>
        <Book.Page.Front>Hello</Book.Page.Front>
        <Book.Page.Back>World</Book.Page.Back>
      </Book.Page>
    </Book>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's a working, draggable book. The play-zone is twice the page width: the sheet rests in the right half and sweeps left as it turns.

Props & API

The essentials, in context:

<Book
  variant="rounded"        // 'flat' (default) | 'rounded'
  width={300}              // page width in px (default 300)
  height={600}             // page height in px (default 600)
  page={page}              // controlled face index (or defaultPage)
  onPageChange={setPage}   // reports the first visible face after a turn
  flippingTime={600}       // programmatic turn duration, ms
  riffleDuration={1000}    // total budget for multi-page riffles, ms
  turnOrigin="bottom"      // 'top' | 'middle' | 'bottom' — corner arc of programmatic turns
  withCover                // rigid first/last pages + compact closed book
  revealBackground="gray.2" // inside-cover base (MantineColor or CSS color)
  curlRadius={90}          // rounded-variant curl tightness
  shadowOpacity={0.5}      // fold shadow strength (0 disables)
  pageBackground="white"   // page base color behind your content
  disabled={false}         // turn off dragging
/>
Enter fullscreen mode Exit fullscreen mode

Gesture tuning: flipThreshold (px to commit a release, default 50), swipeDistance / swipeTimeThreshold (quick-swipe detection, 30px / 250ms), and mobileScrollSupport (default true — vertical scrolling keeps working on touch until a horizontal gesture claims the pointer).

Every visual and gesture prop set on the Book cascades to all pages via context — and any individual Book.Page can override it locally:

<Book variant="rounded" shadowOpacity={0.3} pages={pages}>
  {/* …except this one, which stays flat and shadowless */}
  <Book.Page variant="flat" shadowOpacity={0} /* … */ />
</Book>
Enter fullscreen mode Exit fullscreen mode

For advanced consumers, the pure page-index math (faceToTurnedPages, turnedPagesToFace) and the fold geometry types (Point, ReflectionFold, TurnOrigin) are exported too.

🎨 Styling

Full Mantine Styles API on both levels. The Book exposes root and page selectors; each page exposes root, restSheet, curlSheet, shadowLayer, revealLayer and face, plus CSS variables (--curl-page-width, --curl-page-height, --curl-page-background, --curl-shadow-color, --curl-reveal-background):

<Book
  classNames={{ root: 'my-book' }}
  styles={{ page: { borderRadius: 8 } }}
  pages={pages}
/>
Enter fullscreen mode Exit fullscreen mode

Colors accept any MantineColor ("dark.9", "gray.2") or raw CSS value — theme-aware out of the box.

⚡ Performance

  • Quiescent at rest — no perpetual requestAnimationFrame; the rAF loop runs only during a drag or the release settle.
  • Lazy WebGL — the rounded renderer and its snapshot dependency load on first use; an untouched book costs zero GPU contexts.
  • One context per book — the turn queue guarantees a single page in flight, so the whole stack shares one pooled WebGL context.
  • Memoized stack — a turn re-renders the handful of pages involved, not all N.

Live Demo & Documentation

The documentation site has interactive configurators for every feature — variants, curl radius, covers, riffle, gestures, events — where the displayed code regenerates live from the controls you touch:

gfazioli.github.io/mantine-book

Links

Top comments (0)