Core API Reference

Complete reference for @reelkit/core configuration, callbacks, methods, and state.

SliderController API

The framework-agnostic core. One factory builds a controller from a config and optional events: Config Options are the config, Callbacks the events, and Methods are what the returned controller exposes.

Factory Function

ExportTypeDescription
createSliderController(config: SliderConfig, events?: SliderEvents) => SliderControllerBuild a slider controller. config is required (options below); events is optional (callbacks below). Returns the controller whose methods drive it.

Config Options

PropertyTypeDefaultDescription
countnumberrequiredTotal number of items
initialIndexnumber0Starting index
directionSliderDirection'vertical'Scroll direction, 'vertical' | 'horizontal'
enableGesturesbooleantrueEnable touch/mouse drag navigation. When false, the gesture controller is not attached.
enableNavKeysbooleantrueEnable keyboard arrow key navigation
enableWheelbooleanfalseEnable mouse wheel
wheelDebounceMsnumber200Wheel debounce time
loopbooleanfalseLoop navigation
transitionDurationnumber300Animation duration in ms
swipeDistanceFactornumber0.12Swipe threshold (0-1)
rangeExtractorRangeExtractordefaultRangeExtractorCustom function to determine which indexes are rendered

Callbacks

CallbackTypeDescription
onBeforeChange(index, nextIndex, rangeIndex) => voidBefore slide change
onAfterChange(index, rangeIndex) => voidAfter slide change
onDragStart(index) => voidDrag started
onDragEnd(index) => voidDrag ended
onDragCanceled(index) => voidDrag canceled
onTap(event: GestureCommonEvent) => voidSingle tap (delayed by double-tap window)
onDoubleTap(event: GestureCommonEvent) => voidDouble tap detected
onLongPress(event: GestureCommonEvent) => voidLong press detected
onLongPressEnd(event: GestureEvent) => voidPointer released after long press
onNavKeyPress(increment: -1 | 1) => voidCustom handler for arrow key navigation. Replaces default prev/next behavior.

Methods

MethodTypeDescription
attach(element)(HTMLElement) => voidConnect controller to DOM element for gesture detection
detach()() => voidDetach DOM listeners (gestures, keyboard, wheel). Safe for re-attach via observe(). Use for React effect cleanup.
dispose()() => voidPermanent teardown: detaches all controllers and cleans up signal observers. Use for Angular onDestroy.
observe()() => voidStart gesture, keyboard, and wheel observation. Respects enableGestures, enableNavKeys, and enableWheel config flags.
unobserve()() => voidStop gesture, keyboard, and wheel observation
next()() => Promise<void>Go to next slide
prev()() => Promise<void>Go to previous slide
goTo(index, animate?)(number, boolean?) => Promise<void>Go to specific slide
adjust(duration?)(number?) => voidRecalculate slide positions
setPrimarySize(size)(number) => voidUpdate container size
updateConfig(config)(Partial<SliderConfig>) => voidUpdate configuration options
updateEvents(events)(Partial<SliderEvents>) => voidReplace event handlers (existing handlers not included are preserved)
getRangeIndex()() => numberReturns the position of the active index within the visible range array

State Properties

The controller's state field is a SliderState, the three signals below. Type a value that receives it with SliderState.

PropertyTypeDescription
indexSignal<number>Current slide index
axisValueSignal<AnimatedValue>Current axis position value (animated)
indexesComputedSignal<number[]>Visible indexes for virtualization

Range Extractor

ExportTypeDescription
defaultRangeExtractorRangeExtractorDefault extractor that renders 3 items around current index
RangeExtractor(current: number, count: number, loop: boolean) => number[]Signature of a range extractor. Type your own rangeExtractor with it.

Signal API

Lightweight reactive primitives used throughout the core.

Signal Interface

MemberTypeDescription
valueTGet or set the current value. Setting notifies observers if the value changed.
observe(callback)(callback: () => void) => () => voidRegister a listener called on each value change. Returns a dispose function that removes the listener.

Factory Functions

ExportTypeDescription
createSignal<T>(initial: T) => Signal<T>Create a mutable reactive signal
createComputed<T>(fn: () => T, deps: () => Subscribable[]) => ComputedSignal<T>Create a derived computed signal. The second argument is a deps factory that returns the signals to track.
reaction(deps: () => Subscribable[], effect: () => void) => () => voidRun side effect when any dependency signal changes; returns dispose function. Read signal values inside the effect callback.
batch(fn: () => void) => voidGroup multiple signal updates into a single notification pass; supports nesting
Listener() => voidThe callback observe takes. Type a signal observer you declare separately with it.
Dispose() => voidWhat observe and reaction return: call it to stop listening. Type a stored subscription with it.

Transitions

Built-in transition functions that compute per-slide CSS transforms during animated navigation. Pass one as the transitionTransformFn prop to the framework component.

ExportTypeDescription
TransitionTransformFntypeSignature for custom transition functions
getSlideProgress(axisValue: number, slideIndex: number, primarySize: number) => numberReturns a normalized offset (-1 to 1) for a slide relative to the viewport. Use inside custom transition functions.
slideTransitionTransitionTransformFnDefault slide transition (translateX/Y)
fadeTransitionTransitionTransformFnCrossfade opacity transition
flipTransitionTransitionTransformFn3D card-flip transition
cubeTransitionTransitionTransformFn3D cube rotation transition
zoomTransitionTransitionTransformFnScale/zoom transition
SlideTransformStyle{ transform?; transformOrigin?; opacity?; zIndex? }What a TransitionTransformFn returns for one slide. Return it from a custom transition.

Input Controllers

The slider controller builds these three itself from its enableGestures, enableNavKeys and enableWheel options. Create one directly when you write your own integration, or need gestures, arrow keys or the wheel on an element that is not a slider.

Gesture Controller

ExportTypeDescription
createGestureController(config?: GestureControllerConfig, events?: GestureControllerEvents) => GestureControllerDetects taps, double taps, long presses and drags on an element, and locks each drag to its dominant axis.
GestureController{ attach; detach; observe; unobserve; updateEvents }Object createGestureController returns. attach(element) binds the element, observe() adds the listeners, unobserve() removes them and keeps the element, detach() removes everything; updateEvents(events) merges new handlers in.
GestureControllerConfig{ useTouchEventsOnly?; longPressDurationMs?; doubleTapWindowMs? }Options of createGestureController: ignore the mouse (default false), long-press time (default 800 ms), double-tap window (default 200 ms).
GestureControllerEvents{ onTap?; onDoubleTap?; onTapDown?; onTapUp?; onLongPress?; onLongPressEnd?; onHorizontalDragStart?; onHorizontalDragUpdate?; onHorizontalDragEnd?; onVerticalDragStart?; onVerticalDragUpdate?; onVerticalDragEnd?; onDragEnd? }Callbacks createGestureController calls. A drag goes to the horizontal or vertical handlers by its dominant axis; onDragEnd fires for both.
GestureAxisDragUpdateEventGestureEvent & { delta; distance; primaryDelta; primaryDistance; cancel }Payload of onHorizontalDragUpdate and onVerticalDragUpdate: movement since the last update, total distance, the same along the dominant axis, and cancel() to snap the drag back.
GestureAxisDragEndEventGestureAxisDragUpdateEvent without cancel, plus { velocity; primaryVelocity }Payload of onHorizontalDragEnd and onVerticalDragEnd, with the release speed in pixels per second.
GestureDragEndEventGestureAxisDragEndEvent without the primary fieldsPayload of onDragEnd, for a handler that does not care which axis the drag took.
Offset[x: number, y: number]A pixel pair, used for every position, delta, distance and velocity in a gesture event.
EventKind'touch' | 'mouse'The input a gesture came from, in the kind field of GestureCommonEvent.
DragAxis'horizontal' | 'vertical' | nullThe dominant axis of a drag, null until the first movement decides it.

Keyboard Controller

ExportTypeDescription
createKeyboardController(config: KeyboardControllerConfig, events: KeyboardControllerEvents) => KeyboardControllerTurns arrow key presses into NavKey callbacks.
KeyboardController{ attach; detach }Object createKeyboardController returns. attach(target?) listens for keydown on the target (default window), detach() stops.
KeyboardControllerConfig{ filter?; throttleMs? }Options of createKeyboardController: the keys to react to (default: all four) and the minimum time between two presses (default 0, no throttling).
KeyboardControllerEvents{ onKeyPress: (key: NavKey, event: KeyboardEvent) => void }Callback createKeyboardController calls for each accepted key press.
NavKey'up' | 'right' | 'down' | 'left'The arrow key pressed, as the keyboard controller reports and filters it.

Wheel Controller

ExportTypeDescription
createWheelController(config: WheelControllerConfig, events: WheelControllerEvents) => WheelControllerTurns mouse wheel scrolling into one direction callback per gesture, collapsing the burst of events a single scroll produces.
WheelController{ attach; detach }Object createWheelController returns. attach(target?) listens for wheel on the target (default window), detach() stops and drops a pending callback.
WheelControllerConfig{ debounceMs?; deltaThreshold? }Options of createWheelController: the quiet time that ends a gesture (default 200 ms) and the smallest delta that counts (default 10 px).
WheelControllerEvents{ onWheel: (direction: WheelDirection, event: WheelEvent) => void }Callback createWheelController calls once per scroll gesture.
WheelDirection'up' | 'down' | 'left' | 'right'The direction of a wheel gesture.

Content Loading

Utilities for tracking per-slide loading/error states and preloading media. The loading controller uses an index guard to reject stale callbacks from previously active slides. The preloader uses an LRU cache (default 200 loaded, 100 errored) so revisiting a broken URL shows the error instantly without retry.

ExportTypeDescription
createContentLoadingController() => ContentLoadingControllerPer-slide loading/error state tracking
createContentPreloader(config: ContentPreloaderConfig) => ContentPreloaderLRU-cached media preloader with error caching
observeMediaLoading(video: HTMLVideoElement, callbacks: MediaLoadingCallbacks) => () => voidObserves video loading state (playing, canplaythrough, waiting). Returns a disposer.

ContentLoadingController

ExportTypeDescription
isLoadingSignal<boolean>Whether the active slide is loading
isErrorSignal<boolean>Whether the active slide has errored
setActiveIndex(index: number) => voidUpdate active index, resets loading/error state
onReady(index: number) => voidMark slide as ready (ignored if index doesn't match active)
onWaiting(index: number) => voidMark slide as loading (ignored if index doesn't match active)
onError(index: number) => voidMark slide as errored (ignored if index doesn't match active)

ContentPreloader

ExportTypeDescription
preload(src: string, type?: "image" | "video") => voidStart preloading a media URL
isLoaded(src: string) => booleanCheck if URL is in the loaded LRU cache (max 200)
isErrored(src: string) => booleanCheck if URL is in the error LRU cache (max 100)
markLoaded(src: string) => voidManually mark a URL as loaded
markErrored(src: string) => voidManually mark a URL as errored
onLoaded(src: string, cb: () => void) => () => voidSubscribe to load completion; returns disposer

Sound

Shared mute/unmute state for media playback. The sound controller provides a reactive muted signal that can be synced to video elements and toggled from custom controls.

ExportTypeDescription
createSoundController() => SoundControllerShared mute state controller
syncMutedToVideo(video: HTMLVideoElement, sound: SoundController) => () => voidSyncs the muted signal to a video element. Returns a disposer.

Timeline

Playback timeline controller for video scrubbing. Tracks duration, current time, buffered ranges, and user scrubbing state as reactive signals. A single call wires pointer and keyboard interactions onto any DOM element so it behaves as a native-feeling scrub bar, with pointer capture, live seeking, and full keyboard support (arrows, Home/End, PageUp/PageDown).

ExportTypeDescription
createTimelineController(config?: TimelineControllerConfig) => TimelineControllerFactory returning a controller with duration, currentTime, progress, bufferedRanges, and isScrubbing signals plus attach, detach, bindInteractions, and seek methods.
TimelineControllerConfiginterfacekeyboardStepSeconds (default 5), keyboardPageFraction (default 0.1), and onSeek, onScrubStart, onScrubEnd callbacks.
BufferedRange{ start: number; end: number }A single contiguous buffered region expressed as 0–1 fractions of total duration. Emitted sorted and non-overlapping.

Fullscreen

Cross-browser fullscreen utilities with Safari vendor-prefix guards. The fullscreen signal is a lazy singleton that tracks fullscreen state reactively.

ExportTypeDescription
fullscreenSignalSignal<boolean>Reactive signal tracking whether the document is in fullscreen mode
requestFullscreen(element: HTMLElement) => Promise<void>Enter fullscreen on the given element
exitFullscreen() => Promise<void>Exit fullscreen mode

DOM & Cleanup Utilities

Low-level helpers for DOM event management and deterministic cleanup. Used internally by all controllers and available for custom integrations.

ExportTypeDescription
observeDomEvent(target, event, handler, options?) => () => voidAdds a DOM event listener and returns a disposer that removes it
createDisposableList() => DisposableListComposable list for collecting disposer functions. Call dispose() to run all at once.
createBodyLock() => BodyLockReference-counted body scroll lock. Multiple consumers can lock simultaneously; scroll is restored when all unlock.
sharedBodyLockBodyLockModule-level singleton instance. Use when multiple components across your app should share a single reference counter so nested modals/overlays interleave correctly. Framework bindings (@reelkit/react, @reelkit/vue, @reelkit/angular) use this under the hood.

Utilities

Small helpers the controllers are built from. The framework bindings re-export them, so a custom slide, transition or integration can use the same ones.

ExportTypeDescription
first<T>(arr: readonly T[]) => TFirst element of an array, such as the x of an Offset or the width of a size.
last<T>(arr: readonly T[]) => TLast element of an array, such as the y of an Offset or the height of a size.
generate<G>(count: number, generator: (i: number) => G) => G[]Builds an array of count items from their index, for example generate(10, (i) => i) for demo data.
abs(value: number) => numberAbsolute value.
isNegative(value: number) => booleanWhether a number is below zero, for example to tell a backward drag from a forward one.
clamp(value: number, min: number, max: number) => numberKeeps a number within min and max, inclusive.
lerp(t: number, a: number, b: number) => numberLinear interpolation: a at t = 0, b at t = 1. Use it in a custom transition to map slide progress to a value.
extractRange(count, start, end?, overscan?, loop?) => number[]Indexes from start to end (default start) plus overscan on each side (default 1), wrapped around the ends when loop is true. Build a custom RangeExtractor with it.
noop() => voidA function that does nothing, for a default callback.
createDeferred<T = void>() => Deferred<T>A promise together with the resolve that settles it, for bridging a callback API to await.
Deferred{ promise: Promise<T>; resolve: (value?: T) => void }Object createDeferred returns.
createLruCache<V>(maxSize: number, onEvict?: (value: V, key: string) => void) => LruCache<V>A Map that keeps the maxSize most recently set entries and drops the oldest past that. onEvict runs for each dropped entry, for cleanup such as URL.revokeObjectURL.
LruCacheMap<string, V>Object createLruCache returns. It is a real Map; only set differs.
animate(options: AnimationOptions) => () => voidAnimates a number from from to to with an ease-in-out curve, calling onUpdate each frame. Returns a function that cancels the animation.
AnimationOptions{ from; to; duration; onUpdate; onComplete? }Options of animate: start and end value, duration in milliseconds, the per-frame callback and an optional callback when it finishes.

Size Detection

How every binding and overlay measures its container and the window. Reel uses observeElementSize when no size is passed; the reel player, lightbox and stories player size themselves from the window. SSR-safe: without a window or ResizeObserver the getters return [0, 0] or false and the observers return a no-op disposer.

ExportTypeDescription
observeElementSize(element: Element, callback: (size: [number, number]) => void) => DisposerReports the element's padding box (clientWidth × clientHeight) whenever it changes. Skips a size with a zero side (hidden or not laid out yet) and a size equal to the last one reported.
getViewportSize() => [number, number]The window size as [innerWidth, innerHeight].
observeViewportSize(callback: (size: [number, number]) => void) => DisposerCalls back with the window size on every resize event, including one that leaves the size unchanged (device emulation can switch to a touch device of the same dimensions). Compare sizes yourself if only a change matters.
isMobileWidth(viewportWidth: number) => booleanWhether a window this wide gets the phone layout: 768px and narrower. Every overlay and its stylesheet switch at this width.
fitAspectRatio(viewport: [number, number], aspectRatio?: number) => [number, number]Overlay size for a window. On a phone width it is the whole window; otherwise the full window height at aspectRatio, narrowed to the window width when that would not fit. A missing, zero, negative or non-finite ratio falls back to 9/16.
isTouchDevice() => booleanWhether the device takes touch input (ontouchstart or maxTouchPoints). A capability check, not a screen size: a touch laptop counts, a narrow desktop window does not.

Autoplay

Timers for anything that advances on its own. How they behave and how to wire them is in the Autoplay guide.

ExportTypeDescription
createTimerController(config: TimerControllerConfig) => TimerControllerCounts one duration from 0 to 1 and calls back when it is done. Drives each story in the stories players.
createSlideshowController(config: SlideshowControllerConfig) => SlideshowControllerThe state machine behind a gallery that advances on its own. Drives the lightbox autoAdvance.

createTimerController

TimerControllerConfigTypeDescription
durationnumberDefault duration in milliseconds, used when start() gets none
onComplete() => voidCalled when progress reaches 1
TimerControllerTypeDescription
progressSignal<number>Progress from 0 to 1; drives a progress bar
isRunningSignal<boolean>Whether the timer is counting
start(duration?)(number?) => voidStart over from 0, optionally with another duration for this run
pause()() => voidFreeze at the current progress
resume()() => voidContinue from the frozen progress
reset()() => voidBack to 0, stopped
dispose()() => voidCancel the pending frame

createSlideshowController

SlideshowControllerConfigTypeDescription
intervalnumberHow long an image stays, in milliseconds, counted once it has loaded
onAdvance() => voidThe active slide is done. Move to the next slide, or call pause() on the last one
SlideshowControllerTypeDescription
isPlayingSignal<boolean>Whether the slideshow is switched on. Holds do not change it
progressSignal<number>How far the active image's interval has run, from 0 to 1
play() / pause() / toggle()() => voidSwitch it on or off; the active slide keeps its progress
setInterval(ms)(number) => voidChange the interval from the next image
setSlide(kind)(SlideshowSlideKind) => voidA new slide became active: progress starts over and waits for ready() or ended(). SlideshowSlideKind is 'image' | 'video'
ready()() => voidThe active image loaded or failed to; its interval starts
ended()() => voidThe active video ended; advances now, or once nothing holds
hold(reason) / release(reason)(string) => voidStop the clock for a reason such as 'hover' or 'drag'. Holds stack
dispose()() => voidStop the clock and the visibility listener

Download

Saving a file from a URL, cross-origin included. The fallback it takes is in the Download guide.

ExportTypeDescription
downloadFile(url: string, filename?: string) => Promise<void>Saves a file under filename, by default the last segment of the URL's path. It fetches the file and saves the blob, because the download attribute is ignored on a cross-origin link. When the fetch fails or the server answers with an error (a cross-origin server without CORS headers is the usual case), the URL opens in a new tab instead. Never rejects; does nothing without a window.

Focus Management

Framework-agnostic dialog a11y primitives. The overlay packages use them to return focus to the trigger on close and trap Tab / Shift+Tab inside the overlay while it's open. SSR-safe: each helper returns a no-op disposer in non-browser environments.

ExportTypeDescription
captureFocusForReturn() => DisposerCaptures the currently focused element and returns a disposer that focuses it again. Best-effort: the disposer is a no-op if the captured element has since been removed from the DOM.
createFocusTrap(container: HTMLElement) => DisposerTraps Tab/Shift+Tab inside container. Tab at the last focusable wraps to the first; Shift+Tab at the first wraps to the last; focus that escapes the container (click outside, programmatic focus) is pulled back. Does not move focus into the container on activation — the caller decides.
getFocusableElements(container: HTMLElement) => HTMLElement[]Returns every keyboard-focusable descendant in DOM order, skipping disabled, hidden, and tabindex="-1" elements.

Usage

typescript
import { captureFocusForReturn, createFocusTrap } from '@reelkit/core';

// When your modal opens:
const restoreFocus = captureFocusForReturn();
container.focus({ preventScroll: true });
const releaseTrap = createFocusTrap(container);

// When the modal closes:
releaseTrap();
restoreFocus();

Video Utilities

Framework-agnostic utilities for shared video playback across slides. Used internally by @reelkit/react-reel-player and @reelkit/react-lightbox, available for custom framework bindings.

ExportTypeDescription
captureFrame(video: HTMLVideoElement) => string | nullCaptures the current video frame as a JPEG data URL. Returns null on cross-origin errors.
createSharedVideo(config: SharedVideoConfig) => SharedVideoInstanceCreates a scoped shared video singleton with playback position and frame capture maps. Each consumer gets an isolated instance for iOS sound continuity.
syncVideoObjectFit(video: HTMLVideoElement, fallbackIsVertical: boolean) => DisposerKeeps video.style.objectFit in sync with the video's real orientation. Applies the fallback (from the declared aspect ratio) immediately, then on loadedmetadata reads actual videoWidth / videoHeight and switches to 'cover' for portrait, 'contain' for landscape. Resilient to wrong declared metadata.

URL State

Mirror one query parameter into a signal and back. Two axes, each one job: a codec is the wire (param text ↔ a stable identity), a locator is the lookup (where that identity sits in the collection).

ExportTypeDescription
createUrlStateController({ param, adapter?, codec?, locator? }) => UrlStateControllerMirrors one query parameter into a signal and writes changes back to the URL. The first write of an absent parameter pushes one history entry; every write after that replaces it. Given a codec or locator it also derives position: Signal<Pos | null>, applying the open/close latch and self-healing a parameter that names no slide — so every binding subscribes rather than re-deriving. Writing a position while closed opens at once, without waiting for the adapter to report the write back; UrlChange says when a close steps back versus clears in place.
createHistoryAdapter() => UrlAdapterDefault adapter over the History API. A routed application should inject its own instead, otherwise the router's location goes stale and its next navigation drops the parameter.
indexCodecUrlCodec<number>Reads ?photo=3 as slide 3. Pass it to opt into index derivation without writing a codec of your own. For an infinite or paginated list, pass locator instead — the parameter survives while the promise is pending, so a deep link into an unloaded page is not cleared mid-fetch.
createIndexLocator(countGetter: () => number) => UrlLocator<number>The default index locator: a slide position maps to itself, bounded by the live count the getter returns. An out-of-range index resolves to null, so a stale ?photo=99 self-heals out of the URL — it rejects rather than coercing to the nearest slide, which would open one the URL never named. A getter, not a number, so the bound reads the current size at lookup time as a paginated gallery grows.
urlIndexKey(countGetter, locateAsync?) => UrlKey<number>The matched pair for an index-addressed gallery — indexCodec plus a createIndexLocator bound to the gallery's size. Spread it ({ param, ...urlIndexKey(() => count) }) so the codec cannot drift from the locator. Pass a second locateAsync argument to window a paginated feed — page up to the wanted index on a miss, then return it.
urlIndexTwoAxisKey(opts) => UrlKey<TwoAxisIdentity, TwoAxisPosition>Like urlIndexKey but for a two-axis player: one strictly-dotted ?p=<outer>.<inner> parameter resolving to a TwoAxisPosition { outer, inner }. Options (UrlIndexTwoAxisKeyOptions): outerCount, innerCounts, optional outerCodec/outerLocator for the outer axis, and innerCodec/innerLocate/innerIdentify to address the inner axis by id too. Each axis defaults to a plain index bound. Powers the URL-driven stories player.
createStableIdCodec(hashCodec?: UrlCodec<string>) => UrlCodec<string>The stable-id wire, exported for composing — the param text is the item's id, written raw or transformed by hashCodec (pass base64UrlCodec for reversible base64url). The stable-id analog of indexCodec: pair it with a locator of your own instead of taking the whole urlStableIdKey.
base64UrlCodecUrlCodec<string>The ready-made hash mechanism for a stable-id key: reversible base64url (URL-safe alphabet, no padding, UTF-8) — not a cryptographic hash. Pass it as hashCodec to obscure the id in the URL, or implement your own UrlCodec<string> to plug in a different scheme.
createStableIdLocator(items, locateAsync?) => UrlLocator<string, number>The stable-id lookup, exported for composing — scans items() for a matching id; a gone id resolves to null and self-heals. Optional locateAsync windows a paginated feed. The stable-id analog of createIndexLocator.
urlStableIdKey(opts) => UrlKey<string, number>Addresses a gallery by each item's stable id — ?photo=<id> — instead of its position, so a bookmark survives the list being reordered. Options (UrlStableIdKeyOptions): items (a live getter), optional hashCodec (pass base64UrlCodec) to transform the id on the wire, optional locateAsync to window a paginated feed (fetch until the id is present on a miss, then return its index). Prefer over urlIndexKey whenever the list can change under a shared link.
urlStableIdTwoAxisKey(opts) => UrlKey<TwoAxisIdentity<string>, TwoAxisPosition>The two-axis analog: the outer axis by stable id, the inner by a local index — ?story=user_42.3. Supply innerItems instead of innerCounts to address the inner by id too (?story=user_42.photo_7); hashCodec (e.g. base64UrlCodec) transforms both ids. Options UrlStableIdTwoAxisKeyOptions (index inner) or UrlStableIdTwoAxisIdInnerOptions (id inner); item types satisfy Identified ({ id: string }).
UrlCodec<Id>{ decode(raw) => Id | null; encode(id) => string }The wire format: parameter text ↔ a stable identity, collection-blind. decode returning null means malformed text.
UrlLocator<Id>{ locate(id) => number | null; locateAsync?(id) => Promise<number | null>; identify(index) => id }The lookup: where the identity sits in the collection. locate is synchronous, locateAsync its fallback for a paginated list, and identify turns an index back into an identity for writes.
UrlKey<Id>{ codec: UrlCodec<Id>; locator: UrlLocator<Id> }The matched codec + locator pair for one parameter. They share the same Id and always travel together — the codec spells the identity into the URL, the locator finds where it sits — so building them as a pair is what keeps them from disagreeing.
UrlAdapter{ read, subscribe, push, replace, getState, goBack }The injection point for a router. A routed application must supply one, or the router's own location goes stale. The subscribe listener accepts an optional UrlChange; calling it with nothing is always valid and means the adapter cannot say how the entry came to be current.
UrlChange{ kind?: 'push' | 'replace' | 'pop' }What an adapter knows about the navigation that just landed. Report push only for a navigation the router itself made on the same page; that is the one case where closing may pop the entry. With no evidence the entry is never claimed and closing clears the parameter in place, leaving a duplicate of the page in history rather than risking a step off the site.
UrlStateOptions<Id>{ param: string; adapter?: UrlAdapter; codec?: UrlCodec<Id>; locator?: UrlLocator<Id> }The options createUrlStateController takes — exported so a consumer can type a config assembled separately before handing it over.

Viewed State

Persist how far a viewer got, keyed the same way the address bar is: an entry is the parameter text itself, read back through the same codec and locator.

ExportTypeDescription
createViewedStateController(options) => ViewedStateController<Pos>Remembers how far a viewer got, persisted as the very text a URL parameter would carry. Takes the same codec/locator pair the address bar uses, plus storageKey, optional storage, trackOf (one entry per group), and progressOf. Reads nothing until attach(), so it is safe to prerender.
twoAxisViewedTracking{ trackOf, progressOf }The tracking pair for a two-axis player: one entry per outer slot, the inner index measuring progress through it. Spread it beside a two-axis key.
createLocalStorageAdapter() => StorageAdapterThe default backing. Also createSessionStorageAdapter for state that should not outlive the tab, and createMemoryStorageAdapter for tests and server rendering. Every one absorbs its own failures — an exhausted quota loses that write and no more.
ViewedStateController<Pos>{ entries; resolve(track); record(position); forget(track?); attach() }entries is a signal of track → stored text, so a ring repaints when a position is recorded here or in another tab. resolve runs the full key cycle on every call.
ViewedStateOptions<Id, Pos>{ storageKey; codec; locator; storage?; trackOf?; progressOf; ttlMs?; maxTracks? }The options createViewedStateController takes — exported so a consumer can type a config assembled separately before handing it over. progressOf is optional only for a plain index position, which is its own measure of progress; any other position must say which number to compare, and the type requires it. ttlMs opts into expiry: a track is forgotten that long after it was last recorded, and recording it again restarts its clock. maxTracks keeps at most that many tracks, dropping the least recently recorded on the next write.
StorageAdapter{ read(key); write(key, value); subscribe?(key, listener) }The injection point for a storage layer. Omit subscribe and the store simply runs without cross-tab synchronisation.