
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.

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:
<Bookvariant="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 turnflippingTime={600} // programmatic turn duration, msriffleDuration={1000} // total budget for multi-page riffles, msturnOrigin="bottom" // 'top' | 'middle' | 'bottom' — corner arc of programmatic turnswithCover // rigid first/last pages + compact closed bookrevealBackground="gray.2" // inside-cover base (MantineColor or CSS color)curlRadius={90} // rounded-variant curl tightnessshadowOpacity={0.5} // fold shadow strength (0 disables)pageBackground="white" // page base color behind your contentdisabled={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):
<BookclassNames={{ 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
📦 npm: @gfazioli/mantine-book
📖 Documentation: gfazioli.github.io/mantine-book
🔗 GitHub: gfazioli/mantine-book
Comments (0)
Login to post a comment.