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>
const pages: BookPageData[] = [
{ front: <Cover />, back: <TableOfContents /> },
{ front: <Chapter n={1} />, back: <Chapter n={2} /> },
];
<Book width={260} height={360} pages={pages} />
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} /* ... */ />
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>
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} />
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
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>
);
}
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
/>
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>
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}
/>
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
- 🦄 Mantine Extensions Hub

Top comments (0)