Documentation
API reference
Look up Segue's exported functions, components, hooks, callback options, results, defaults, and errors.Use @humaan/segue for serializable manifest functions and types. Use @humaan/segue/client only from Client Components or other browser code.
Server import
The root entry point doesn't import React client or browser APIs:
import {
defineManifest,
normalizePath,
type ImageReference,
type ImageRendition,
type JsonValue,
type ManifestInput,
type ManifestRoute,
type ManifestRouteInput,
type SegueManifest,
} from "@humaan/segue";Manifest types
type JsonValue =
| null
| boolean
| number
| string
| readonly JsonValue[]
| { readonly [key: string]: JsonValue };
interface ImageReference {
readonly assetId: string;
readonly profile: string;
}
interface ImageRendition<Data = JsonValue> extends ImageReference {
readonly data: Data;
}
interface ManifestRoute<Preset extends string = string> {
readonly path: string;
readonly preset: Preset;
readonly imagesByRole: Readonly<Record<string, ImageReference>>;
}
interface SegueManifest<Preset extends string = string, RenditionData = JsonValue> {
readonly routes: readonly ManifestRoute<Preset>[];
readonly renditions: readonly ImageRendition<RenditionData>[];
}
interface ManifestRouteInput<Preset extends string, RenditionData> {
readonly path: string;
readonly preset: Preset;
readonly imagesByRole: Readonly<
Record<string, ImageRendition<RenditionData>>
>;
}
interface ManifestInput<Preset extends string, RenditionData> {
readonly routes: readonly ManifestRouteInput<Preset, RenditionData>[];
}ManifestRouteInput contains full Image Renditions in imagesByRole. defineManifest converts them to Image References and deduplicates full renditions into SegueManifest.renditions.
defineManifest
function defineManifest<Preset extends string, RenditionData = JsonValue>(
input: ManifestInput<Preset, RenditionData>,
): SegueManifest<Preset, RenditionData>;Each input route has this contract:
| Field | Type | Constraint |
|---|---|---|
path | string | Starts with exactly one /; contains no query or hash; unique after normalization. |
preset | Preset | Non-empty string. |
imagesByRole | Readonly<Record<string, ImageRendition<RenditionData>>> | Plain object with non-empty role keys. |
Each Image Rendition requires a non-empty string assetId, a non-empty string profile, and a data property. The input, routes, imagesByRole, and rendition values must be plain data. Rendition data supports JSON-compatible primitives, dense arrays, and plain objects; it rejects cycles, non-finite numbers, class instances, and other non-JSON values.
import { defineManifest } from "@humaan/segue";
const manifest = defineManifest({
routes: [
{
path: "/projects/field-study/",
preset: "project-cover",
imagesByRole: {
hero: {
assetId: "field-study-hero",
profile: "route-hero",
data: { src: "/images/field-study.jpg", width: 1600, height: 900 },
},
},
},
],
});The returned manifest is a deep-frozen copy. Its route path is /projects/field-study; its imagesByRole.hero contains only the assetId and profile; and the full data appears once in renditions. Reusing an assetId and profile pair with unequal data throws.
normalizePath
function normalizePath(path: string): string;normalizePath applies URL pathname normalization, including . and .. segment resolution, and removes all trailing slashes except the root slash.
normalizePath("/projects/./field-study/"); // "/projects/field-study"
normalizePath("/projects/field-study/../civic-centre"); // "/projects/civic-centre"
normalizePath("/"); // "/"The input must start with one slash. Protocol-relative paths beginning with //, queries, and hashes throw. The function accepts a pathname, not a complete URL.
createSegue
function createSegue<
Preset extends string,
RenditionData,
TransitionImageProps extends object = Record<string, never>,
>(): SegueApplication<Preset, RenditionData, TransitionImageProps>;
interface SegueApplication<Preset extends string, RenditionData, TransitionImageProps extends object> {
readonly Provider: ComponentType<
SegueProviderProps<Preset, RenditionData, TransitionImageProps>
>;
readonly Link: ForwardRefExoticComponent<
SegueLinkProps & RefAttributes<HTMLAnchorElement>
>;
readonly TransitionImage: ComponentType<
TransitionImageComponentProps<TransitionImageProps>
>;
readonly definePreset: <const ImageRole extends string>(
definition: Omit<
PresetDefinition<Preset, RenditionData, TransitionImageProps>,
"imageRoles" | "render"
> & {
readonly imageRoles: readonly ImageRole[];
readonly render: ComponentType<
PresetRenderProps<TransitionImageProps, ImageRole>
>;
},
) => PresetDefinition<Preset, RenditionData, TransitionImageProps>;
readonly useNavigation: () => {
readonly navigate: (options: NavigateOptions) => Promise<NavigationResult>;
};
readonly usePendingDestination: () => string | null;
readonly useRouteCommitPending: () => boolean;
readonly useRouteTransitionEntry: () => RouteTransitionEntry<Preset> | null;
}Call createSegue once for one application contract. It returns Provider, Link, TransitionImage, definePreset, useNavigation, usePendingDestination, useRouteCommitPending, and useRouteTransitionEntry.
"use client";
import { createSegue } from "@humaan/segue/client";
type PresetName = "project-cover" | "section-wipe";
type RenditionData = { src: string; width: number; height: number };
type TransitionImageProps = { alt: string; className?: string; sizes?: string };
export const Segue = createSegue<
PresetName,
RenditionData,
TransitionImageProps
>();Every component and hook on that object uses private contexts created by the same call. Don't mix a Provider from one createSegue result with a component or hook from another.
definePreset and createPreset
Segue.definePreset preserves literal image-role names so TypeScript restricts the role-based TransitionImage passed to render.
function definePreset<const ImageRole extends string>(
definition: Omit<
PresetDefinition<Preset, RenditionData, TransitionImageProps>,
"imageRoles" | "render"
> & {
readonly imageRoles: readonly ImageRole[];
readonly render: ComponentType<
PresetRenderProps<TransitionImageProps, ImageRole>
>;
},
): PresetDefinition<Preset, RenditionData, TransitionImageProps>;
interface PresetDefinition<Preset extends string, RenditionData, TransitionImageProps extends object> {
readonly imageRoles: readonly string[];
readonly reducedMotion: "run" | "skip";
readonly render: ComponentType<PresetRenderProps<TransitionImageProps>>;
readonly animate: (
options: PresetAnimateOptions<Preset, RenditionData>,
) => void | Promise<void>;
}
interface PresetRenderProps<TransitionImageProps extends object, ImageRole extends string = string> {
readonly TransitionImage: ComponentType<
TransitionImageProps & { readonly role: ImageRole }
>;
}PresetAnimateOptions contains the full run options plus the mounted host:
interface PresetRunOptions<Preset extends string, RenditionData> {
readonly navigation: RouteNavigation;
readonly preset: Preset;
readonly route: ManifestRoute<Preset>;
readonly imagesByRole: Readonly<Record<string, ImageRendition<RenditionData>>>;
}
interface PresetAnimateOptions<Preset extends string, RenditionData>
extends PresetRunOptions<Preset, RenditionData> {
readonly root: HTMLElement;
}definePreset returns the definition unchanged and performs no runtime validation itself. The Provider validates definitions referenced by the manifest. With reducedMotion: "skip", Segue doesn't render or animate the Preset and doesn't warm its declared roles when reduced motion is preferred. With "run", your callback must implement an appropriate reduced-motion result. The navigation decision is captured when Segue accepts navigation; warming checks the current preference when each warming event occurs.
createPreset creates only a typed definition helper:
function createPreset<
RenditionData,
TransitionImageProps extends object,
>(): <const ImageRole extends string>(
definition: Omit<
PresetDefinition<string, RenditionData, TransitionImageProps>,
"imageRoles" | "render"
> & {
readonly imageRoles: readonly ImageRole[];
readonly render: ComponentType<
PresetRenderProps<TransitionImageProps, ImageRole>
>;
},
) => PresetDefinition<string, RenditionData, TransitionImageProps>;Use it for shared Preset modules that shouldn't depend on an application's complete Preset union.
Provider
interface SegueProviderProps<Preset extends string, RenditionData, TransitionImageProps extends object> {
readonly children: ReactNode;
readonly manifest: SegueManifest<Preset, RenditionData>;
readonly presets: Record<Preset, PresetDefinition<Preset, RenditionData, TransitionImageProps>>;
readonly presetLifecycle?: PresetLifecycle<Preset, RenditionData>;
readonly renderImage?: ImageRenderer<RenditionData, TransitionImageProps>;
readonly imageWarmingEnabled?: boolean;
readonly transitionTimeoutMs?: number;
readonly commitTimeoutMs?: number;
readonly onIssue?: (issue: SegueIssue) => void;
}| Prop | Default | Contract |
|---|---|---|
children | Required | Application subtree that can use this Segue object's components and hooks. |
manifest | Required | Static manifest snapshot captured at first mount. Prop changes don't rebuild route or rendition indexes. |
presets | Required | Definition for every Preset in the generic union; every manifest route's named definition must exist and be valid. Updated definitions apply to later navigation. |
presetLifecycle | undefined | Optional acquire and beforeCommit callbacks for Presets. |
renderImage | undefined | Required if any supplied Preset declares at least one image role, even when no route currently uses that Preset. Also required by rendered Transition Images and Link-level warming images. |
imageWarmingEnabled | true | Enables new passive and interaction Image Warming requests. It doesn't remove renditions already warmed or change renderImage validation. |
transitionTimeoutMs | 3000 | Positive finite milliseconds for the asynchronous Override-to-Preset sequence to settle. Callbacks must not block synchronously. |
commitTimeoutMs | 15000 | Positive finite milliseconds from Next.js navigation start until the tracked navigation settles. |
onIssue | Logs to console.error | Receives recoverable runtime issues. If it throws, Segue catches that error and logs Segue onIssue callback failed. |
An accepted navigation snapshots its Preset definition, image roles, reduced-motion decision, timeout values, and image renderer. Updating those Provider props affects later navigation, not work already accepted. The Preset Lifecycle is captured when its Preset starts, and onIssue uses the current callback when an issue is reported.
Preset lifecycle callbacks
interface PresetLifecycle<Preset extends string, RenditionData> {
readonly acquire?: (
options: PresetRunOptions<Preset, RenditionData>,
) => void | (() => void);
readonly beforeCommit?: (
options: PresetRunOptions<Preset, RenditionData>,
) => void | Promise<void>;
}acquire runs immediately before Segue mounts a selected Preset. Its optional cleanup must be synchronous, idempotent, and non-throwing. It runs when navigation settles or ownership is aborted by a timeout, history navigation, or Provider unmount. A rejected animate or beforeCommit continues navigation; the host and lifecycle remain acquired until navigation settles or is otherwise abandoned. beforeCommit runs after animate resolves and immediately before Segue starts Next.js navigation. If acquire, animate, or beforeCommit throws or rejects, Segue reports preset-failed.
Image renderer callback
interface ImageRenderOptions<
RenditionData,
TransitionImageProps extends object,
> {
readonly image: ImageRendition<RenditionData>;
readonly props?: TransitionImageProps;
}
type ImageRenderer<RenditionData, TransitionImageProps extends object> = (
options: ImageRenderOptions<RenditionData, TransitionImageProps>,
) => ReactNode;Image Warming omits props and mounts the renderer's output in a hidden, accessibility-hidden container. Warmed renditions remain mounted until Provider unmount. A Transition Image receives the props supplied to its component. With next/image, provide alt="" and loading="eager" as renderer defaults, then let Transition Image props override alt.
Mount one Provider per browser window, including across duplicate package copies. Mounting another throws from an effect. On unmount, Segue aborts active work, clears subscribers, and releases the browser-window registration.
TransitionImage
type TransitionImageComponentProps<TransitionImageProps extends object> =
TransitionImageProps & { readonly image: ImageReference };Segue.TransitionImage resolves the exact assetId and profile pair from the Provider's static rendition index, then calls renderImage with the component's typed props.
<Segue.TransitionImage
image={{ assetId: "field-study-hero", profile: "route-hero" }}
alt="A field researcher beside a wetland"
sizes="100vw"
/>Use this form in source or destination UI. Inside a Preset, use the role-based TransitionImage passed to render. The role-based form looks up route.imagesByRole[role] and then delegates to Segue.TransitionImage.
Link
type SegueLinkProps =
& Omit<NextLinkProps, "href">
& Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof NextLinkProps>
& {
readonly href: NextLinkProps["href"];
readonly children?: ReactNode;
readonly routeTransition?: false | RouteTransitionOptions;
};
interface RouteTransitionOptions {
readonly override?: RouteTransitionOverride;
readonly images?: readonly ImageReference[];
}
type RouteTransitionOverride = (
navigation: RouteNavigation,
) => boolean | Promise<boolean>;Segue.Link forwards its ref and inherits all non-conflicting Next.js Link and anchor props. These are the navigation-relevant props:
| Prop | Default | Behavior |
|---|---|---|
href | Required | Next.js string or URL object. as is used as the coordinated browser URL when present. |
children | undefined | Link contents. |
routeTransition | undefined | Uses the destination Preset. false disables the Override, Preset, and Image Warming for this Link, but Segue still coordinates different-path pending state, history, and commit. An object can provide override and extra warming images. |
replace | false | Selects push or replace. |
scroll | true | Forwarded to Next.js and exposed as optional navigation.scroll when explicitly supplied. |
prefetch | null in the App Router | Forwarded unchanged; accepts true, false, "auto", or null and remains separate from Segue Image Warming. |
as | undefined | Optional displayed URL; Segue resolves this value for coordination when supplied. |
transitionTypes | undefined | Forwarded to Next.js and copied for programmatic router calls. |
onNavigate | undefined | Runs before Segue. Calling preventDefault() prevents all Segue work. |
onClick | undefined | Forwarded to Next.js; onNavigate is the callback that gates Segue coordination. |
onMouseEnter, onFocus, onTouchStart | undefined | Your handler runs first, then Segue requests interaction warming. |
| Anchor props | Browser defaults | Includes className, style, target, download, ARIA attributes, data attributes, and other anchor events. |
The navigation callback has this exact shape. Pointer, focus, touch, and click handlers use the corresponding React anchor event-handler types.
onNavigate?: (event: { preventDefault: () => void }) => void;The Override receives:
interface RouteNavigation {
readonly href: string;
readonly method: "push" | "replace";
readonly signal: AbortSignal;
readonly scroll?: boolean;
readonly source: HTMLElement | null;
}For a Link, source is its anchor. Return true when the Override handled the visual work. Return false to use the destination Preset. If the callback throws or rejects, Segue reports override-failed and tries the Preset. The images array only adds Image References to warming; it isn't passed to the Override. A non-empty images array requires renderImage, even if warming is disabled.
Segue leaves external URLs and same-path query or hash changes to Next.js. While another coordinated navigation is active, a different-path, same-origin Link that requires coordination is ignored without changing the URL or exposing a result. The component doesn't set disabled, aria-disabled, aria-busy, or inert automatically.
useNavigation and navigate
const { navigate } = Segue.useNavigation();
interface NavigateOptions {
readonly href: string;
readonly method?: "push" | "replace";
readonly routeTransition?: false | RouteTransitionOptions;
readonly scroll?: boolean;
readonly source?: HTMLElement | null;
readonly transitionTypes?: readonly string[];
}
type NavigationResult =
| { readonly status: "navigated" }
| { readonly status: "ignored"; readonly reason: "busy" | "same-location" };| Option | Default | Behavior |
|---|---|---|
href | Required | Valid relative or absolute string resolved against window.location.href. |
method | "push" | Uses router or document push/assign; "replace" uses router or document replace. |
routeTransition | undefined | Selects the destination Preset. false disables visual work but keeps the coordinated navigation lifecycle. It also accepts the same override and images options as Link; programmatic images aren't warmed because navigate has no warming events. |
scroll | undefined | Omits the router option and preserves Next.js default behavior. |
source | null | Element exposed to the Override. |
transitionTypes | undefined | When set, copied into a mutable array and passed to the Next.js router. |
URL handling follows this order:
- If another navigation is active, resolve
{ status: "ignored", reason: "busy" }. - Reject
javascript:URLs, including control-character-obfuscated spellings. - Resolve
hrefagainst the current URL. - For a different origin, use
location.assignorlocation.replaceand resolve as navigated. - For the same normalized pathname, query, and hash, resolve
{ status: "ignored", reason: "same-location" }. - For the same normalized pathname with a different query or hash, call the Next.js router immediately without Route Transition state.
- For a different pathname, select the Override or exact manifest Preset, run outgoing work, and then call the Next.js router.
The Promise resolves { status: "navigated" } after Segue starts router or document navigation, not after navigation settles. A push may internally become replace after Segue creates its cancellable temporary history entry.
Route state hooks
type RouteTransitionEntry<Preset extends string> =
| { readonly type: "preset"; readonly preset: Preset }
| { readonly type: "override" }
| { readonly type: "none" };| Lifecycle point | usePendingDestination() | useRouteCommitPending() | useRouteTransitionEntry() for current pathname |
|---|---|---|---|
| Initial load | null | false | null |
| Different-path navigation accepted | Normalized destination path | false | Existing current-route value |
| Override or Preset running | Destination path | false | Existing current-route value |
| Outgoing work settled; router started | Destination path | true | { type: "none" } for the still-current source pathname |
| Requested destination commits | null | false | { type: "preset", preset }, { type: "override" }, or { type: "none" } |
| Navigation settles without the requested destination | null | false | { type: "none" } for the actual current pathname |
| Uncoordinated pathname commit | null | false | { type: "none" } |
| Outgoing timeout; router started | Destination path | true | { type: "none" } for the still-current source pathname |
| Cancellation during outgoing work | null | false | Previous committed value |
| Commit-timeout cleanup | null | false | { type: "none" } for the still-current source pathname |
usePendingDestination() reports navigation pending state, not whether visual work exists. It clears when the tracked navigation settles or is canceled.
useRouteCommitPending() becomes true only after outgoing work settles and remains true until navigation settles or is canceled. Same-path query or hash navigation doesn't change it.
useRouteTransitionEntry() is keyed to the current normalized pathname. Its value describes the selected handoff, not animation success: a failed Preset still produces a Preset entry when navigation commits. It returns null only on initial load before any route entry has been recorded.
All hooks use useSyncExternalStore and throw if called outside the matching Segue Provider.
Reported issues
onIssue receives this shape:
interface SegueIssue {
readonly code: SegueIssueCode;
readonly message: string;
readonly cause?: unknown;
}SegueIssueCode | When it is reported | Recovery |
|---|---|---|
transition-timeout | The asynchronous Override-to-Preset sequence doesn't settle within transitionTimeoutMs. | Abort outgoing work and start navigation. |
commit-timeout | The tracked Next.js navigation doesn't settle within commitTimeoutMs. | Abort state and use document navigation. |
navigation-failed | The Next.js router call throws. | Use document navigation. |
override-failed | Override throws or rejects. | Try the destination Preset. |
preset-failed | Preset Lifecycle acquisition, Preset animation, or beforeCommit throws or rejects. | Continue navigation. |
missing-required-image | A selected Preset role has no resolvable route rendition. | Skip that Preset's work and continue navigation. |
history-prepare-failed | Segue can't write the temporary cancellable push entry. | Reset state and use document navigation. |
history-cleanup-failed | Segue can't clear temporary state after commit. | Keep the committed route and report the stale state. |
history-recovery-failed | Stale history recovery throws or replacement fails. | Report; replacement is attempted where possible. |
The default handler calls console.error with [Segue: code], the issue message, and cause.
Thrown errors
Recoverable navigation failures use onIssue. Invalid configuration, invalid usage, and blocked URLs throw:
| API | Condition |
|---|---|
defineManifest | Input isn't plain data with a routes array. |
defineManifest | A route or imagesByRole isn't a plain object. |
defineManifest / normalizePath | Path doesn't start with exactly one slash, or contains a query or hash. |
defineManifest | Duplicate normalized route path. |
defineManifest | Empty Preset, role, assetId, or profile; invalid rendition; or missing data. |
defineManifest | Rendition data uses an unsupported non-JSON value, non-finite number, cycle, sparse array, or non-plain object. |
defineManifest | One assetId and profile pair has conflicting data. |
Provider | transitionTimeoutMs or commitTimeoutMs isn't a positive finite number. |
Provider | A manifest Preset definition is missing or has invalid animate, render, imageRoles, or reducedMotion. |
Provider | A manifest-referenced Preset contains an empty or non-string image role. |
Provider | Any supplied Preset declares image roles without renderImage. |
Provider | A second Provider, including one from a duplicate package copy, mounts in the browser window. |
Segue.TransitionImage | Image Reference isn't in the static rendition index, or renderImage is absent. |
Preset role TransitionImage | Requested role wasn't resolved for the selected route. |
Segue.Link | routeTransition.images is non-empty without renderImage. |
| Components and hooks | Used outside the Provider created by the same createSegue call. |
navigate | href is a javascript: URL. |
navigate | href can't be parsed relative to the current browser URL. |
| Preset or image rendering | render or renderImage throws during React rendering. These errors aren't reported through onIssue. |
waitForAnimation
function waitForAnimation(
animation:
| {
cancel(): void;
readonly finished: PromiseLike<unknown>;
}
| {
stop(): void;
then(onResolve: VoidFunction, onReject?: VoidFunction): Promise<void>;
},
signal: AbortSignal,
): Promise<void>;This helper supports native Web Animations API Animation objects, Motion controls, and other stoppable thenables. Motion isn't a Segue dependency; install motion separately before importing from it.
import { animate } from "motion";
import { waitForAnimation } from "@humaan/segue/client";
const projectCover = Segue.definePreset({
imageRoles: [],
reducedMotion: "skip",
render: () => <div className="project-cover" />,
animate: ({ root, navigation }) => {
const cover = root.querySelector<HTMLElement>(".project-cover");
if (!cover) return;
return waitForAnimation(
animate(cover, { x: ["-100%", "0%"] }),
navigation.signal,
);
},
});Guard the nullable DOM query before calling Motion's animate; passing null isn't part of this contract.
| Outcome | waitForAnimation behavior |
|---|---|
| Signal already aborted | Calls cancel() or stop() once, doesn't read finished or call then(), and resolves unless cancellation throws. |
| Signal aborts while waiting for a native animation | Calls cancel() and resolves without waiting for finished. |
| Signal aborts while waiting for a stoppable thenable | Calls stop() and remains pending until the animation thenable settles. Motion's stop() settles its controls. |
| Animation fulfills | Resolves void and removes the abort listener. |
| Native animation rejects before abort | Rejects with the same failure and removes the abort listener. |
| Stoppable thenable rejects | Rejects with the same failure and removes the abort listener. |
stop() doesn't settle the thenable | The helper remains pending; stoppable implementations must settle after stop(). |
Public exports
| Entry point | Runtime values | Types |
|---|---|---|
@humaan/segue | defineManifest, normalizePath | ImageReference, ImageRendition, JsonValue, ManifestInput, ManifestRoute, ManifestRouteInput, SegueManifest |
@humaan/segue/client | createPreset, createSegue, waitForAnimation | ImageRenderer, ImageRenderOptions, NavigateOptions, NavigationMethod, NavigationResult, PresetAnimateOptions, PresetDefinition, PresetLifecycle, PresetRenderProps, PresetRunOptions, ReducedMotionBehavior, RouteNavigation, RouteTransitionEntry, RouteTransitionOptions, RouteTransitionOverride, SegueApplication, SegueIssue, SegueIssueCode, SegueLinkProps, SegueProviderProps, TransitionImageComponentProps |