Documentation
Core concepts
Learn how destination paths, Presets, Transition Images, Overrides, and route state work together.Segue runs a Cover on the current route and then asks Next.js to navigate. The Cover can remain visible while Segue waits for navigation to settle. Segue then removes it; animation inside the destination remains the responsibility of that route.
Select a Preset by destination
The destination pathname normally selects the Route-Transition Preset. The manifest assigns one Preset to each exact path:
import { defineManifest } from "@humaan/segue";
export const segueManifest = defineManifest({
routes: [
{
path: "/projects/field-study",
preset: "project-cover",
imagesByRole: {},
},
],
});Any Segue.Link that targets /projects/field-study uses project-cover, regardless of which route contains the link. Queries and hashes don't affect the selection.
Route-Transition Presets
A Route-Transition Preset defines the reusable Cover for navigation to a destination. Each Preset has four properties:
| Property | Purpose |
|---|---|
render | Returns the temporary elements displayed above the current route. |
animate | Starts the Cover after React adds those elements to the document. |
reducedMotion | Chooses whether to run or skip the Preset when the visitor prefers reduced motion. |
imageRoles | Lists any named Transition Images that the Cover needs. |
Segue renders the temporary elements before it calls animate. The callback receives the Preset's root element, navigation details, route data, and Preset name. If animate returns a Promise, Segue waits for it before asking Next.js to navigate.
If the callback throws or its Promise rejects, Segue reports the problem and continues navigation. Segue doesn't require a specific animation library.
Outgoing Ownership
Outgoing Ownership starts immediately before Segue invokes an Override or starts a Preset run. It ends when navigation settles or Segue abandons the visual work.
In the normal path, Segue runs the Cover, starts Next.js navigation, keeps the Preset mounted while it waits, and removes the Preset when navigation settles. During this time, another coordinated navigation is treated as busy.
Segue also removes the Preset when a timeout, browser history navigation, or Provider unmount interrupts the transition. The navigation.signal passed to Presets and Overrides lets their animation code stop at the same time.
Outgoing Ownership doesn't decide how the rest of the page behaves. Your application decides whether to lock scrolling, prevent interaction with the old route, show loading feedback, or move focus after navigation.
Transition Images
Use Transition Images when a Cover reproduces an image that also appears on the destination route. If the Cover doesn't include a destination image, you don't need this feature.
For visual continuity, the Cover's final state and the destination's initial state should render the same Image Rendition. When both use the same image resource, the browser can reuse what it has already loaded instead of flashing an unloaded image after route commit.
An Image Rendition is one transformed version of an Image Asset. Each rendition contains:
assetId, which identifies the source Image Asset.profile, which identifies a size, crop, format, or other transformation.data, which contains everything your renderer needs, such assrc,width, andheight.
Together, assetId and profile identify one Image Rendition. Every use of that pair must provide the same rendition data.
The manifest assigns each destination rendition to an image role such as hero. This complete example uses the shared types from the getting-started tutorial:
// lib/segue-manifest.ts
import { defineManifest } from "@humaan/segue";
import type { PresetName, RenditionData } from "./segue-types";
export const segueManifest = defineManifest<PresetName, RenditionData>({
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 },
},
},
},
],
});Define alt as a required Transition Image prop. A source or destination image can then provide useful alternative text, while decorative Preset UI can pass an empty string.
// lib/segue-types.ts
export type TransitionImageProps = {
readonly alt: string;
readonly className?: string;
readonly sizes?: string;
};The Provider's renderImage function controls how Segue renders the rendition. For Image Warming, Segue omits props and mounts the result in a hidden, accessibility-hidden container. The alt="" default satisfies next/image; visible Transition Image props override it with useful alternative text. Set loading="eager": a lazily loaded image inside the hidden warming container might never load.
// components/segue-image.tsx
"use client";
import type { ImageRenderer } from "@humaan/segue/client";
import Image from "next/image";
import type { RenditionData, TransitionImageProps } from "@/lib/segue-types";
export const renderSegueImage: ImageRenderer<
RenditionData,
TransitionImageProps
> = ({ image, props }) => (
<Image
alt=""
loading="eager"
{...props}
src={image.data.src}
width={image.data.width}
height={image.data.height}
/>
);The Preset declares the roles it requires. Inside render, its role-based TransitionImage resolves hero from the destination route. The Preset host has aria-hidden="true", so this decorative copy uses alt="".
// components/route-transitions.tsx
"use client";
import type { ComponentProps, ReactNode } from "react";
import { waitForAnimation } from "@humaan/segue/client";
import { segueManifest } from "@/lib/segue-manifest";
import { renderSegueImage } from "./segue-image";
import { Segue } from "./segue";
const projectCover = Segue.definePreset({
imageRoles: ["hero"],
reducedMotion: "skip",
render: ({ TransitionImage }) => (
<div className="project-cover">
<TransitionImage
role="hero"
className="project-cover__image"
sizes="100vw"
alt=""
/>
</div>
),
animate: async ({ root, navigation }) => {
const cover = root.querySelector<HTMLElement>(".project-cover");
if (!cover) return;
const animation = cover.animate(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 450, easing: "ease-out", fill: "forwards" },
);
await waitForAnimation(animation, navigation.signal);
},
});
const presets = {
"project-cover": projectCover,
} satisfies ComponentProps<typeof Segue.Provider>["presets"];
export function RouteTransitions({ children }: { readonly children: ReactNode }) {
return (
<Segue.Provider
manifest={segueManifest}
presets={presets}
renderImage={renderSegueImage}
>
{children}
</Segue.Provider>
);
}The animation's fill: "forwards" retains the final full-screen image until route commit. Render the destination's initial image with the same dimensions and crop so removing the Preset at commit doesn't cause a visual jump.
Segue.TransitionImage accepts an image reference containing { assetId, profile }, resolves its canonical manifest data, and delegates to the same renderer:
// app/projects/field-study/page.tsx
"use client";
import { Segue } from "@/components/segue";
export default function FieldStudyPage() {
return (
<main className="project-hero">
<Segue.TransitionImage
image={{ assetId: "field-study-hero", profile: "route-hero" }}
className="project-hero__image"
sizes="100vw"
alt="A field researcher recording observations beside a wetland"
/>
<h1>Field study</h1>
</main>
);
}.project-cover,
.project-hero {
position: fixed;
inset: 0;
}
.project-cover__image,
.project-hero__image {
width: 100%;
height: 100%;
object-fit: cover;
}You can also use Segue.TransitionImage for source UI used by an Override. Inside a Preset, use the role-based component passed to render instead.
Image Warming gives the browser an earlier opportunity to load the rendition. Segue starts a hidden render when a link comes within 300 pixels of the viewport. It skips this passive warming when navigator.connection.saveData is true or the effective connection type is "slow-2g" or "2g".
Mouse enter, keyboard focus, and touch start trigger interaction warming even on those constrained connections. Set imageWarmingEnabled={false} if your own network or product policy should disable new warming requests. Each warmed rendition is deduplicated and remains mounted until the Provider unmounts, so keep warming renderers free of effects that assume a short-lived mount. Warming might not finish, and Segue never delays navigation for it.
Route-Transition Overrides
A Route-Transition Override runs source-specific Cover code instead of the destination's standard Preset. Use one when the animation must modify an element on the current route, such as the exact card a visitor selected.
Segue calls the Override before navigation. The callback receives the destination, navigation method, source element when available, and AbortSignal:
- Return
truewhen the Override handled the Cover. - Return
falsewhen Segue should use the destination Preset instead. - If the callback throws or rejects, Segue reports the problem and also tries the Preset.
This complete Override uses the Link anchor as navigation.source, declines when reduced motion is preferred or no project card exists, cancels its animation through navigation.signal, and returns true only after it handles the Cover:
"use client";
import {
waitForAnimation,
type RouteNavigation,
} from "@humaan/segue/client";
import { Segue } from "./segue";
async function coverSelectedCard(navigation: RouteNavigation): Promise<boolean> {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
return false;
}
const card = navigation.source?.closest<HTMLElement>("[data-project-card]");
if (!card) return false;
const animation = card.animate(
[{ transform: "scale(1)" }, { transform: "scale(1.08)" }],
{ duration: 400, easing: "ease-out", fill: "forwards" },
);
await waitForAnimation(animation, navigation.signal);
return true;
}
export function ProjectCardLink() {
return (
<article data-project-card>
<h2>Field study</h2>
<Segue.Link
href="/projects/field-study"
routeTransition={{ override: coverSelectedCard }}
>
View project
</Segue.Link>
</article>
);
}An Override doesn't receive temporary UI from Segue. It normally animates elements from the current route, which can unmount at route commit. If that UI needs a manifest image, render it with Segue.TransitionImage before navigation.
Route state
Segue publishes state as a navigation moves through the current lifecycle. Route-Transition Entry records whether Segue selected a Preset, an Override, or no visual handoff.
| Point in the lifecycle | Published state |
|---|---|
| Navigation accepted | Pending Destination contains the target pathname. |
| Cover running | Pending Destination remains set. |
| Waiting for Next.js | Route Commit Pending becomes true. |
| Navigation settles | Both pending values clear. When the destination commits, its Route-Transition Entry becomes available. |
useNavigation starts programmatic navigation. The other three hooks read the state described above:
| Hook | Meaning |
|---|---|
usePendingDestination | Returns the target pathname until navigation settles or is canceled. |
useRouteCommitPending | Returns true after outgoing work finishes and until navigation settles or is canceled. |
useRouteTransitionEntry | Reports whether Segue selected a Preset, an Override, or no visual handoff for the current route. |
Use pending state for concrete navigation feedback rather than inferring loading from the visual phase:
"use client";
import { Segue } from "./segue";
export function RouteStatus() {
const pendingDestination = Segue.usePendingDestination();
const routeCommitPending = Segue.useRouteCommitPending();
const entry = Segue.useRouteTransitionEntry();
const message = routeCommitPending
? "Opening the destination..."
: pendingDestination
? "Preparing the transition..."
: "";
return (
<p
role="status"
aria-live="polite"
data-route-entry={entry?.type ?? "initial"}
>
{message}
</p>
);
}Route-Transition Entry doesn't guarantee that the selected animation completed. For example, it can report a Preset even if that Preset failed and Segue continued navigation. The value is null on initial load.
Failure and reduced motion
Animation errors don't permanently block navigation. If an Override declines or fails, Segue tries the destination Preset. If a Preset fails, Segue calls the Provider's onIssue callback and asks Next.js to navigate anyway. By default, onIssue logs to console.error.
If the asynchronous Override-to-Preset sequence doesn't settle within transitionTimeoutMs, Segue cancels it and starts navigation. Callbacks must still avoid blocking synchronous work. If Next.js navigation doesn't settle within commitTimeoutMs, Segue falls back to a full-page navigation.
Every Preset must choose a reduced-motion behavior. With "skip", Segue doesn't render the Preset, run the Cover, or warm its declared Transition Images. With "run", your animation code must provide suitable reduced-motion behavior.
Segue captures the Preset's reduced-motion decision when it accepts navigation. Preference changes during active navigation apply to later navigation.
Use waitForAnimation with navigation.signal to stop native Web Animations API animations, Motion controls, and similar animation objects when the transition ends.