turn-ts-svelte
v0.1.1
Published
Svelte 5 bindings for turn-ts. A page-flip book component built on runes, shipped as Svelte source.
Maintainers
Readme
turn-ts-svelte
Svelte 5 bindings for turn-ts, the framework-agnostic
page-flip engine. Two components, a bindable api, and runes throughout — no store shims, no
createEventDispatcher.
[!IMPORTANT] Not open source. Non-commercial use only. The runtime dependency
turn-tsis 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-sveltesvelte ^5 is a peer dependency. turn-ts is a direct dependency — you do not install it
yourself, but you do import its stylesheet.
Use
<script lang="ts">
import { TurnBook, TurnPage } from 'turn-ts-svelte';
import 'turn-ts/turn-ts.css'; // required — the fold has no styles without it
</script>
<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.
Bound page
<script lang="ts">
import { TurnBook, TurnPage } from 'turn-ts-svelte';
let page = $state(1);
</script>
<TurnBook bind:page width={840} height={560}>
{#each chapters as chapter (chapter.id)}
<TurnPage>{@html chapter.body}</TurnPage>
{/each}
</TurnBook>
<p>Page {page}</p>With bind:page the value is two-way: set it and the book turns there, drag the book and the value
follows once the turn lands. Without the binding, page still works as the starting page and as a
programmatic target — the book just has nowhere to report back to. defaultPage is the same
starting-page role for code that never wants to touch page at all.
"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 book
only writes back when the value it holds has gone off screen, so a bound 5 is not silently
rewritten to 4 for a spread that never moved.
The imperative api
<script lang="ts">
import { TurnBook, TurnPage, type TurnBookApi } from 'turn-ts-svelte';
let api = $state<TurnBookApi | null>(null);
</script>
<TurnBook bind:api width={840} height={560}>…</TurnBook>
<button type="button" onclick={() => api?.previous()}>Previous</button>
<button type="button" onclick={() => api?.next()}>Next</button>bind:api holds the live TurnBookApi — next, previous, page, pages, view,
range, size, display, configure, resize, update, disable, stop, animating,
hasPage, addPage, removePage, destroy. It is null before onMount has run and null
again after the component is destroyed. destroy is the component's to call; calling it yourself
leaves the component holding a dead book.
TurnBook also exports next(), previous(), goTo(page) and getApi() for bind:this, which
is the shape this package had before it moved onto turn-ts:
<script lang="ts">
let book: ReturnType<typeof TurnBook>;
</script>
<TurnBook bind:this={book}>…</TurnBook>
<button type="button" onclick={() => book.next()}>Next</button>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 | Bindable. Current page, 1-based. |
| defaultPage | number | Starting page when page has no value. |
| 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 api still work. |
| api | TurnBookApi \| null | Bindable. null before mount and after destroy. |
Callback props: 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. Every other attribute you pass (id,
role, aria-*, tabindex, onkeydown, class, style, data-*) lands on the container
untouched.
If createTurnBook throws and you have not passed oniniterror, the error is rethrown from
onMount. A book that silently did not build is worse.
TurnPage props
Renders the host-owned shape the engine documents:
<div class="turn-page-wrapper"> <!-- Svelte'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 Svelte 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. class, style and every other attribute land on the
.turn-page; wrapperClass and wrapperStyle reach the wrapper, and bind:element gives you the
.turn-page node.
Pages are read from the DOM in order, so key an {#each} 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 on the container is .turn-book, and the only inline
styles it writes there are --turn-book-width and --turn-book-height. Colour, borders, shadows
and type are yours. Do not set width/height in style — those belong to the engine. 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 attributes pass straight through:
<TurnBook
bind:api
width={840}
height={560}
role="region"
aria-label="Field notes"
tabindex={0}
onkeydown={(event) => {
if (event.key === 'ArrowRight') api?.next();
if (event.key === 'ArrowLeft') api?.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
- SSR is safe.
turn-tstouches no DOM at module scope,onMountdoes not run on the server and neither do effects. The container renders sized from CSS custom properties and the fold arrives on mount. - The package ships Svelte source.
svelte-packageemitsdist/*.svelteplus the generated.d.ts, so your bundler compiles the components against your own Svelte version. There is no CJS build, because there is no Svelte 5 runtime worth putting in one. - The
pagesoption is not exposed. It truncates by callingremovePage, which throws for the host-owned wrappersTurnPagerenders. Render fewer children instead. - The engine re-measures itself on
pointerdownand through aResizeObserver. Callapi.resize()by hand only if you defeat both.
Develop
npm install
npm run lint # svelte-check + tests + svelte-package + publintTests run on Vitest with Happy DOM and a real mount(). 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, teardown, the bindable api and page, option pushing, dynamic children, initialization
failure reporting, attribute pass-through, and unmounting mid-fold without a NotFoundError.
License
Non-commercial. See LICENSE.md — turn.js 3rd release terms, inherited through
turn-ts.
