@gfazioli/mantine-book
A realistic iBooks-style book component for React, built on Mantine. Stack two-sided pages and turn them by dragging any point of the free edge in any direction — a pure-DOM reflection fold (flat) or a true 3D WebGL curl (rounded), with controlled page navigation.
Installation
After installation import package styles at the root of your application:
You can import styles within a layer @layer mantine-book by importing @gfazioli/mantine-book/styles.layer.css file.
Usage
Book renders pages you can turn with a realistic iBooks-style page curl.
Each Book.Page is the physical entity that turns: it always has two sides,
declared as Book.Page.Front and Book.Page.Back. Grab any point of the
page's free edge and drag in any direction to fold it. The play-zone is twice
the page width: the page rests in the right half and sweeps toward the spine
(the centre) as it curls; once turned, it rests in the left half and folds
back the other way. The simplest book is a single page:
Faces
A page has two faces, declared as compound children: Book.Page.Front (the
resting side) and Book.Page.Back (revealed as the page turns). Each face
can hold any React node — text, images, MDX, even a <canvas> — rendered once
and owned by React, so event handlers inside a face stay alive.
Book.Page.Back is optional: omit it and the back renders blank (filled with
the page background). Toggle it below:
Content and alignment
Each face clips overflow and centers smaller content by default. Use the
align prop on the Book for every face, or per-face on Book.Page.Front /
Book.Page.Back to override. It accepts { horizontal, vertical }, each one
of start / center / end, and only affects content smaller than the face:
Media content
img, svg, video and canvas are automatically scaled to fit the face
(object-fit: contain), so oversized artwork is letterboxed rather than
cropped — or size them to the face with object-fit: cover for a full-page
image:

Variants and curl shape
The page turn ships two rendering paths, selected with the variant prop
(set it on the Book for every page, or per page to mix them):
| variant | Renderer | Notes |
|---|---|---|
| flat (default) | Pure-DOM reflection fold (CSS clip-path + gradient) | Fully interactive at rest, SSR-safe, the universal fallback |
| rounded | True 3D curl drawn on a WebGL canvas | Faces are a static snapshot during the curl; the wrap tightness is set by curlRadius |
The default flat variant needs no canvas and works everywhere. If WebGL is
unavailable or a face snapshot fails, rounded falls back to flat
automatically, so it is always safe to opt in. Switch the variant and drag the
page to compare them:
Curl radius
curlRadius (rounded only, in px, default ~0.32 × width) sets how tightly the
page wraps: a smaller radius makes a tight roll that turns the page sooner, a
larger one a gentle, loose curl. Drag the slider, then drag the page:
Dimensions
width and height size the pages in CSS px, set once on the Book and
applied to every page. The interactive play-zone is twice the width: the
pages rest in the right half and the curl sweeps left toward the spine, so a
width={260} book occupies a 520px-wide box.
Multi-page books
Stack several pages and the Book behaves like a real book. Drag the right half to turn the current page forward and the left half to turn the previous one back. While a page curls, the page beneath shows through — the next one going forward, the previous one going back. Visual and gesture props set on the Book (variant, shadows, timings…) are inherited by every page via optional context, and any page can override them locally.
The page state follows the usual Mantine controlled/uncontrolled pattern:
page + onPageChange for controlled, defaultPage for uncontrolled. The
index counts faces: page i has its front at 2i and its back at 2i + 1,
so 0 shows the closed book and 1 shows the first page turned. Both faces
of a spread are equivalent as a setter; onPageChange reports the first
visible face in reading order.
Data-driven pages
Instead of children, pass a pages array of { front, back, props? }
objects — handy when the book is built from data (a gallery, a CMS, a PDF
manifest). The optional props entry carries per-page overrides and wins
over the Book's inherited props:
Standalone page
A Book.Page also works on its own, outside any Book — it sizes itself
with its own width / height and turns like a single sheet (standalone,
the whole play-zone is the grab surface; grabZone="sheet" restricts the
grab to the resting page itself). The resting side is also controllable:
pass flipped together with onFlip and an external change runs the same
animated turn as a drag. Inside a Book, the book's width / height
apply to every page instead:
External navigation
Because page is controlled, the book can be driven entirely from outside —
arrows, a pagination, keyboard shortcuts. A programmatic page change runs the
same settle animation as a completed drag: turnOrigin picks the simulated
grab point — bottom or top curl from that corner with a diagonal crease,
middle folds the page straight over — and flippingTime sets the speed
(tune both below). Dragging the pages keeps working alongside the controls.
Turns are queued, one page in flight at a time: rapid clicks or key
presses never overlap two animations — they accumulate and the book riffles
through them in order, compressing the per-page duration on long jumps so
the whole run fits the riffleDuration budget (one second by default —
slow it down below to watch every page go by). The in-between pages of a
riffle turn with the flat fold whatever the variant — at riffle speed the
curl is indistinguishable, and the flat path keeps long runs smooth — while
the final landing page turns with the book's variant. Try hammering the
next arrow.
Covers
withCover treats the book as a physical bound volume. The first and
last pages turn rigid — the whole sheet rotates flat around the spine
under the play-zone perspective, like a hard cover, while the inner pages
keep their soft curl (a page can opt in or out with its own hard prop).
Rigid pages are pure DOM — no WebGL even in a rounded book, so covers are
always crisp and cost nothing on the GPU. And the closed book is
compact: at either end the single visible page is centered on the
play-zone, sliding into the two-page spread as the cover opens — the slide
is a CSS transition, disabled under prefers-reduced-motion. Drag the cover
slowly to feel the difference: it stays flat all the way over.
Large books
The page stack is built to scale: a page costs its resting DOM and nothing
more. With variant="rounded" the whole book shares one WebGL context
(created lazily on the first turn), and only the touchable spread keeps its
face snapshots warm — a buried page captures on demand the moment its turn
starts, and the in-between steps of a riffle skip the WebGL pipeline
entirely. The per-page warmSnapshots prop exposes that same switch for a
standalone page. So the 100-page book below costs the same GPU resources as
a single sheet — each page's hue advances by the golden angle, so
neighbouring pages contrast strongly while the whole book still spans the
color wheel. Drag the slider for a long riffle:
Shadows and lighting
shadowOpacity (0–1) controls how strongly the lifting page is shaded as it
curves away from the light, and shadowColor (a MantineColor or any CSS
color) tints that shadow. Both apply to either variant.
On top of the shadow, the rounded variant also lights its own 3D surface:
a Lambert term darkens the curve, a tight specular ridge highlights the apex,
and the light automatically follows the direction you fold so the gloss reads
in both turn directions. This lighting needs no configuration — it is part of
the 3D render, which is why there is no separate lighting control.
Page background and reveal
pageBackground (a MantineColor or any CSS color) fills each face behind its
content, so transparent content sits on a solid page.
revealBackground paints what the curl uncovers. On the Book it is the
inside cover: a base under the whole stack, visible where no page rests (the
left half before the first turn, the right half past the last) and through
the curl of the first and last page — between two inner pages the natural
reveal is the next page itself. On a single Book.Page it paints a layer
under that page only, for standalone peel effects.
Interaction and release
When you release a drag the page settles with an animation over flippingTime
ms. flipThreshold (0–100, percent of a full turn) is the cutoff: drag past it
and the page completes the turn; below it, it settles back to rest. disabled
removes the drag interaction entirely — on the Book for every page, or on a
single Book.Page to pin just that one.
Touch gestures
On touch devices the page turn also recognizes a swipe — a quick flick
that completes the turn even when you release below flipThreshold. A
gesture counts as a swipe only when it travels at least swipeDistance px in
under swipeTimeThreshold ms, so raising swipeDistance (or lowering
swipeTimeThreshold) demands a longer / faster flick.
mobileScrollSupport (default true) keeps vertical page scrolling alive:
the component waits for a clearly horizontal gesture before claiming the
touch, so a vertical drag that starts on the book still scrolls the page. Set
it to false to capture every touch on the book immediately (page scroll
from the book is then blocked).
These tune swipe recognition on touch devices — try them on a phone or a trackpad.
Keyboard shortcuts
The book is keyboard-operable out of the box: focus it (it is in the tab
order) and turn the pages from the keyboard — every keyboard turn plays the
same animated curl as the external controls, and collapses to an instant
settle under prefers-reduced-motion: reduce. Keys are handled only while
the book itself has focus, so interactive content inside a face keeps its
own keyboard behavior.
| Key | Action |
|---|---|
| → | Turn the current page forward |
| ← | Turn the previous page back |
| Home | Jump to the front cover (closed book) |
| End | Jump past the last page (back cover) |
For assistive technology the root exposes role="group" with
aria-roledescription="book" (give it your own name with aria-label), and
a polite live region announces the visible pages after every turn — "Page 1
of 6" when a single page is visible, "Pages 2–3 of 6" on an open spread. Use
pageAnnouncement to localize or customize the message:
Events
onFold fires throughout an interaction with { progress, phase }: phase
is 'grab' the instant the pointer takes the page (before any movement),
'move' while dragging, and 'settle' during the release animation.
onFlip fires once when a settle finishes, with the resting { flipped }
state — both are per page, on Book.Page. The Book-level onPageChange
fires when a completed turn changes the page. Drag the page and watch the
readout:
onFold: { progress: 0, phase: '—' }
onFlip: { flipped: false }
Styles API
Book supports the full Mantine Styles API.
Use classNames, styles, vars and unstyled to customize the book
(root, the per-page page wrapper) and every layer of a page — the
resting sheet, the lifting flap, the rigid cover sheet, the shadow and
reveal layers, the face content wrapper. Use the interactive playground
below to inspect every selector and CSS variable; the full reference for
both levels lives in the Styles API tab above.
Component Styles API
Hover over selectors to highlight corresponding elements
