npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

turn-ts-react

v0.1.1

Published

React 19 bindings for turn-ts. A page-flip book component with an explicit "use client" boundary.

Readme

turn-ts-react

React 19 bindings for turn-ts, the framework-agnostic page-flip engine. Two components, an imperative escape hatch on ref, and an explicit "use client" boundary so it drops straight into a Next.js App Router app.

[!IMPORTANT] Not open source. Non-commercial use only. The runtime dependency turn-ts is a derivative of turn.js (3rd release), whose licence permits use "solely for personal benefit and not for any commercial purpose or for monetary gain." That restriction reaches this package through the dependency and cannot be removed here. Read LICENSE.md before installing.

Install

npm i turn-ts-react

react and react-dom ^19 are peer dependencies. turn-ts is a direct dependency — you do not install it yourself, but you do import its stylesheet.

Use

'use client';

import { TurnBook, TurnPage } from 'turn-ts-react';
import 'turn-ts/turn-ts.css'; // required — the fold has no styles without it

export function Book() {
  return (
    <TurnBook width={840} height={560} display="double">
      <TurnPage>Cover</TurnPage>
      <TurnPage>Page two</TurnPage>
      <TurnPage>Page three</TurnPage>
      <TurnPage>Back</TurnPage>
    </TurnBook>
  );
}

turn-ts/turn-ts.css is not optional and is not bundled into this package — it carries the rules the fold needs, and just as importantly it leaves out the ones that break it (no overflow, no contain, no perspective on the container). Import it once, anywhere, or copy its rules into your own sheet.

Controlled

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

<TurnBook width={840} height={560} page={page} onPageChange={setPage}>
  {chapters.map((chapter) => (
    <TurnPage key={chapter.id}>{chapter.body}</TurnPage>
  ))}
</TurnBook>;

With page set the prop is the source of truth: it is re-asserted after every render, so a drag that lands somewhere else is reported through onPageChange and then turned back unless you accept it. Drop page and pass defaultPage instead for an uncontrolled book that just tracks its own position.

"Already on page" means in view, not equal. A display="double" book shows a spread, so page={4} and page={5} hold the same two leaves open and neither turns to the other. The engine reports the leading page of the spread, which is what onPageChange gives you.

The imperative api

const book = useRef<TurnBookApi>(null);

<TurnBook ref={book} width={840} height={560}>…</TurnBook>;

book.current?.next();
book.current?.page(7);
book.current?.size(1024, 680);

ref is the React 19 ref-as-a-prop, holding the live TurnBookApinext, previous, page, pages, view, range, size, display, configure, resize, update, disable, stop, animating, hasPage, addPage, removePage, destroy. It is null before the mount effect has run and null again after unmount. destroy is the wrapper's to call; calling it yourself leaves the component holding a dead book until it unmounts.

TurnBook props

| Prop | Type | Notes | | --- | --- | --- | | width, height | number | Whole book in px — both leaves in display="double". Omit to size from the container. | | page | number | Makes the book controlled. | | defaultPage | number | Starting page for an uncontrolled book. Ignored when page is set. | | display | 'single' \| 'double' | | | gradients | boolean | | | duration | number | Turn duration in ms. | | cornerSize | number | Size of the grabbable corner hit area in px. | | corners | 'backward' \| 'forward' \| 'all' \| ('tl'\|'tr'\|'bl'\|'br')[] | Which corners fold. | | disabled | boolean | Refuses pointer folds. Turns through the ref still work. | | ref | Ref<TurnBookApi> | The ref object's value is null before mount and after unmount. |

Events: onReady, onStart, onTurning, onTurn, onTurned, onFirst, onLast, plus onPageChange(page, view) alongside onTurned and onInitError(error). All but onStart get a TurnBookPageEvent of { type, api, page, view }; onStart additionally gets the corner that was grabbed and a preventDefault() that refuses the fold.

None of those names collide with a native <div> handler — onError is the near miss, which is why the initialization hook is onInitError. Everything else you pass (id, role, aria-*, tabIndex, onKeyDown, className, style, data-*) lands on the container untouched.

If createTurnBook throws and you have not passed onInitError, the error is rethrown from the effect and reaches your nearest error boundary. A book that silently did not build is worse.

TurnPage props

Renders the host-owned shape the engine documents:

<div class="turn-page-wrapper">      <!-- React's node. The engine styles it, never moves it. -->
  <div class="turn-page">            <!-- The engine's node to fold. -->
    <div class="turn-page-content">  <!-- Your children, clipped to the page box. -->

The wrapper is the component's root, so it is the only node React ever inserts into or removes from the book — which is what keeps a re-render mid-fold from throwing NotFoundError at a page element the engine has lifted out. className, style, ref and every other prop land on the .turn-page; wrapperClassName and wrapperStyle reach the wrapper.

Pages are read from the DOM in order, so key them the way you would key any list — insert one in the middle and it becomes the middle page.

Styling and accessibility

Headless: the only class this package renders is .turn-book, and the only inline styles it writes on the container are --turn-book-width and --turn-book-height. Colour, borders, shadows and type are yours. Do not set width/height in the style prop — those belong to the engine, and React would fight it for them on every render. Use the width/height props.

The book has no keyboard affordance of its own, because a page-turn is a visual convenience and the navigation belongs to your app. Give it one — the props pass straight through:

const book = useRef<TurnBookApi>(null);

<TurnBook
  ref={book}
  width={840}
  height={560}
  role="region"
  aria-label="Field notes"
  tabIndex={0}
  onKeyDown={(event) => {
    if (event.key === 'ArrowRight') book.current?.next();
    if (event.key === 'ArrowLeft') book.current?.previous();
  }}
>
  …
</TurnBook>;

Pages outside the current view are hidden with display: none, so assistive technology sees only what is open. Give each TurnPage real headings and put anything that must always be reachable — a table of contents, a "read as one page" fallback — outside the book.

Notes

  • createTurnBook runs in an effect, never during render. It touches the DOM immediately, so there is no SSR pass to worry about; the container renders sized from CSS custom properties and the fold arrives on mount.
  • StrictMode is fine. The mount/unmount/mount cycle destroys the first book before the second is built, which is exactly the contract the engine's one-book-per-element claim expects.
  • The pages option is not exposed. It truncates by calling removePage, which throws for the host-owned wrappers TurnPage renders. Render fewer children instead.
  • The engine re-measures itself on pointerdown and through a ResizeObserver. Call ref.current.resize() by hand only if you defeat both.

Develop

npm install
npm run lint    # typecheck + tests + build + "use client" check + publint

Tests run on node --test through tsx, with Happy DOM registered as the global DOM and React's own act. Happy DOM has no layout engine, so fold geometry is not covered here — that is turn-ts's problem. What is covered is this package's: mount timing, StrictMode, ref lifecycle, controlled and uncontrolled paging, dynamic children, and unmounting mid-fold without a NotFoundError.

License

Non-commercial. See LICENSE.md — turn.js 3rd release terms, inherited through turn-ts.