@humaan/segue
v0.1.0
Published
Consumer-owned Route Transition coordination for Next.js
Readme
@humaan/segue
Segue coordinates consumer-owned Route Transitions around Next.js App Router navigation. Version 0.1.0 runs a Cover before navigation, keeps its temporary UI mounted while Next.js finishes the navigation, and then removes it. Your application supplies the markup, CSS, animation, and production interface behavior.
Use Segue when you need project-specific transition visuals assigned to exact destination paths. It isn't a ready-made animation component, and 0.1.0 doesn't run a post-commit Reveal or a Destination Entrance.
Quick start
This example adds a visible green Cover before navigation from / to /projects/field-study.
1. Check the requirements and install
You need:
- Next.js
>=16.3.1 <17 - React
>=19.2 <20 - The Next.js App Router
pnpm add @humaan/segueThe package doesn't include CSS, an animation library, or a build plugin.
2. Define the destination manifest
Create lib/segue-manifest.ts. The explicit generics make the manifest's Preset and rendition data types identical to the types passed to createSegue in the next step.
import { defineManifest } from "@humaan/segue";
export type PresetName = "project-cover";
export type RenditionData = {
src: string;
width: number;
height: number;
};
export const segueManifest = defineManifest<PresetName, RenditionData>({
routes: [
{
path: "/projects/field-study",
preset: "project-cover",
imagesByRole: {},
},
],
});The root @humaan/segue export is server-safe. Each manifest entry matches one normalized destination pathname; it doesn't match a dynamic route pattern.
3. Create the typed client integration
Create components/route-transitions.tsx:
"use client";
import type { ReactNode } from "react";
import {
createSegue,
waitForAnimation,
type PresetDefinition,
} from "@humaan/segue/client";
import {
segueManifest,
type PresetName,
type RenditionData,
} from "../lib/segue-manifest";
type TransitionImageProps = {
className?: string;
};
type RouteTransitionsProps = Readonly<{
children: ReactNode;
}>;
const Segue = createSegue<
PresetName,
RenditionData,
TransitionImageProps
>();
const presets = {
"project-cover": Segue.definePreset({
imageRoles: [],
reducedMotion: "skip",
render: () => <div className="segue-cover" data-cover />,
animate: async ({ root, navigation }) => {
const cover = root.querySelector<HTMLElement>("[data-cover]");
if (!cover) throw new Error("The project Cover did not mount");
const animation = cover.animate(
[
{ transform: "translateX(-100%)" },
{ transform: "translateX(0)" },
],
{
duration: 600,
easing: "cubic-bezier(.76, 0, .24, 1)",
fill: "forwards",
},
);
await waitForAnimation(animation, navigation.signal);
},
}),
} satisfies Record<
PresetName,
PresetDefinition<PresetName, RenditionData, TransitionImageProps>
>;
export function RouteTransitions({ children }: RouteTransitionsProps) {
return (
<Segue.Provider manifest={segueManifest} presets={presets}>
{children}
</Segue.Provider>
);
}
export function ProjectLink() {
return (
<Segue.Link href="/projects/field-study">
View the field study
</Segue.Link>
);
}A Route-Transition Preset defines its temporary UI and Cover animation. reducedMotion: "skip" prevents Segue from mounting or animating this Preset when the visitor prefers reduced motion.
4. Mount the Provider and add the CSS
Mount one Provider per browser window in a layout that remains mounted across the source and destination routes. The root layout is usually the correct location.
// app/layout.tsx
import type { ReactNode } from "react";
import { RouteTransitions } from "../components/route-transitions";
import "./globals.css";
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en">
<body>
<RouteTransitions>{children}</RouteTransitions>
</body>
</html>
);
}/* app/globals.css */
.segue-cover {
position: fixed;
inset: 0;
z-index: 100;
pointer-events: none;
background: #b8f34a;
transform: translateX(-100%);
}Segue marks the temporary Preset host aria-hidden="true". Keep controls, announcements, and meaningful content outside it.
5. Add the routes and run the application
// app/page.tsx
import { ProjectLink } from "../components/route-transitions";
export default function HomePage() {
return (
<main>
<h1>Projects</h1>
<ProjectLink />
</main>
);
}// app/projects/field-study/page.tsx
export default function FieldStudyPage() {
return (
<main>
<h1>Field study</h1>
</main>
);
}pnpm devOpen the local URL and select View the field study. The green Cover crosses the current route, Next.js navigates, and Segue removes the Cover when /projects/field-study commits.
Behavior and ownership
- Manifest routes are exact normalized pathnames. An unlisted destination still navigates without a Preset.
- A Route-Transition Override can provide one-off consumer choreography. Returning
false, throwing, or rejecting falls back to the destination's Preset. routeTransition={false}skips the Override, Preset, and Image Warming, but Segue still coordinates pending state, history, and navigation settlement.navigate()resolves after Segue invokes router or document navigation, not after route settlement. It can instead resolve{ status: "ignored", reason: "busy" }or{ status: "ignored", reason: "same-location" }.- Presets and Overrides receive
navigation.signal.waitForAnimation()connects that signal to native Web Animations API animations or stoppable thenable controls.
Version 0.1.0 removes Preset UI when navigation settles and doesn't provide a post-commit Reveal. Your application owns visual design, reduced-motion policy, scroll and interaction behavior, focus, accessibility, monitoring, and Destination Entrance animation.
Documentation
Continue with the user documentation source:
Internal design material:
TERMINOLOGY.jsondefines released public language.CONTEXT.mdrecords the versioned maintainer model.docs/adrcontains internal architecture decision records, not user guides.docs/specs/0.2.0-reveal-phase.mdis a proposed internal 0.2 specification. Its Reveal API isn't available in0.1.0.
