router
Every export of @kitr/router, @kitr/router/routes, @kitr/router/stack, read from the published type definitions. The guide shows them in use.
@kitr/router
Re-exports @tanstack/history, @tanstack/react-router, @tanstack/router-core unchanged, plus the following.
can
function can(permission: string, options?: {
subject?: unknown;
to?: string;
}): ({ context }: {
context: {
session?: GuardSession | null;
};
}) => void;
beforeLoad permission guard: passes when context.session.can(permission, subject) is true.
Otherwise throws a FORBIDDEN KitrError (403) that the route's error screen renders, or redirects when to is given.
compileTree
function compileTree(entries: RouteEntry[], options?: CompileTreeOptions): AnyRoute;
Build the TanStack route tree from the table's page entries.
createKitrRouter
function createKitrRouter<TRouteTree extends AnyRoute>(options: CreateKitrRouterOptions<TRouteTree>): Router<TRouteTree, 'never', true>;
function createKitrRouter<R>(options: CreateKitrRouterFromRoutesOptions<R>): Router<TreeOf<R>, 'never', true>;
createRouter with kitr's defaults: queryClient and app in context, intent
preload, scroll restoration, default pending/error/not-found components,
and the app's providers wrapped around the whole tree.
With @kitr/query installed, loaders can context.queryClient.ensureQueryData(...).
DefaultError
function DefaultError({ error, reset }: ErrorComponentProps): import("react").JSX.Element;
The default error screen: the KitrError's message (or "Something went wrong"), a "Try again" button that reloads the route and a "Go home" link. FORBIDDEN errors read "You don't have access to this page."
DefaultNotFound
function DefaultNotFound(): import("react").JSX.Element;
The default not-found screen: "Page not found" and a "Go home" link, for unknown URLs and thrown notFound().
DefaultPending
function DefaultPending(): import("react").JSX.Element;
The default loading screen: a spinner with "Loading…" for screen readers (data-kitr="pending").
createKitrRouter uses it for every route; override with defaultPendingComponent or a route's pendingComponent.
definePage
function definePage(page: PageModule): PageModule;
export default definePage({ component: HomePage, pendingComponent: HomeSkeleton }) in a page file.
guards
function guards<Ctx>(...fns: Array<(ctx: Ctx) => unknown | Promise<unknown>>): (ctx: Ctx) => Promise<Record<string, unknown>>;
Compose several beforeLoad guards into one. They run in order and are awaited; the objects
they return are merged into the route context, and the first throw stops the rest.
onlyPlatform
function onlyPlatform(platforms: string | string[], options?: OnlyPlatformOptions): ({ context }: {
context: {
platform?: GuardPlatform | null;
};
}) => void;
beforeLoad guard that limits a route to some platforms: the marketing pages exist on the
www build only, the account pages on the authed web app and the phone. Keys are platform
names from kitrrc.ts, platform types (web, mobile), OS names or native. Other platforms
get the route's not-found page, or a redirect with to. Reads context.platform (the service
from @kitr/platform, put in context by createKitrRouter) and falls back to the build's
KITR_PLATFORM / KITR_PLATFORM_TYPE, so it works with no platform package at all.
export const Route = createFileRoute('/pricing')({ beforeLoad: onlyPlatform('www') })
requireAuth
function requireAuth(options?: RequireAuthOptions): ({ context, location }: {
context: {
session?: GuardSession | null;
app?: AppWithConfig;
};
location: {
href: string;
};
}) => {
session: GuardSession;
};
beforeLoad guard. Reads context.session (set by @kitr/auth or your own
provider) and redirects anonymous users, remembering where they were going.
export const Route = createFileRoute('/_app')({ beforeLoad: requireAuth() })
pages
const pages: {
readonly key: "pages";
readonly kinds: readonly [
"page",
"page-layout",
"page-group",
"page-root"
];
readonly methods: {
readonly root: (this: AnyRouteTable, options: Record<string, unknown>) => AnyRouteTable;
readonly page: (this: AnyRouteTable, path: string, options?: Record<string, unknown>) => AnyRouteTable;
readonly layout: (this: AnyRouteTable, id: string, ...rest: unknown[]) => AnyRouteTable;
};
readonly group: (child: AnyRouteTable, parent: AnyRouteTable, prefix: string, options: GroupOptions) => void;
readonly targets: {
readonly tanstack: typeof compileTree;
};
};
The pages extension for the kitr route table: .root, .page and .layout on
defineRoutes([pages], ...), compiled to a TanStack Router tree by createKitrRouter({ routes }).
import pages from '@kitr/router/routes'
export const routes = defineRoutes([pages], (r) =>
r.root({ head, component: RootDocument }).page('/', { lazy: () => import('./pages/home') }),
)
CompileTreeOptions
interface CompileTreeOptions {
/** The current platform name; pages whose `platforms` leave it out are dropped. */
platform?: string;
/** Default root route options when the table has no `.root(...)`. */
root?: Record<string, unknown>;
}
What compile('tanstack', options) receives from createKitrRouter.
CreateKitrRouterBaseOptions
interface CreateKitrRouterBaseOptions<TRouteTree extends AnyRoute> extends Omit<RouterOptions<TRouteTree, 'never', false, any, any>, 'context' | 'routeTree'> {
/** The app from `app/app.ts`. Put in every route's context as `context.app`; forked per request on the server. */
app: KitrApp;
/** Defaults to the container's `queryClient` (from @kitr/query). */
queryClient?: QueryClient;
/** Extra context merged into every route (e.g. `session`). */
context?: Record<string, unknown>;
/** The platform this router is built for. Default `import.meta.env.KITR_PLATFORM`. */
platform?: string;
}
Options of createKitrRouter: TanStack's router options without context, plus the kitr app and extra context.
CreateKitrRouterFromRoutesOptions
interface CreateKitrRouterFromRoutesOptions<R> extends CreateKitrRouterBaseOptions<TreeOf<R>> {
routes: PageRoutes<R>;
routeTree?: never;
}
createKitrRouter({ routes }): the kitr route table from app/routes.ts, compiled to a route tree.
CreateKitrRouterOptions
interface CreateKitrRouterOptions<TRouteTree extends AnyRoute> extends CreateKitrRouterBaseOptions<TRouteTree> {
routeTree: TRouteTree;
routes?: never;
}
createKitrRouter({ routeTree }): a TanStack route tree, from createRootRoute().addChildren or file-based routes.
GuardPlatform
interface GuardPlatform {
name?: string | undefined;
type?: string;
native?: boolean;
os?: string;
}
What onlyPlatform() reads: the platform service when @kitr/platform is installed, else the build's name and type.
GuardSession
interface GuardSession {
/** The signed-in user. `requireAuth()` redirects when it is missing or null. */
user?: unknown | null;
/** Answers `can(permission, { subject })`. Without it, every `can()` check fails. */
can?: (permission: string, subject?: unknown) => boolean;
}
What the guards read from context.session: a user when signed in, and an optional permission check.
KitrPageOptions
interface KitrPageOptions {
/** How the page enters in a stack outlet: slide in (`push`, default), slide up over the parent (`cover`), bottom sheet, or full screen. */
presentation?: 'push' | 'cover' | 'sheet' | 'fullscreen';
/** Keep the page mounted (state, scroll, media) when another page covers it. */
keepAlive?: boolean;
/** The page a cold deep link stacks under this one (`/chats/$id` under `/chats`). */
stackParent?: string;
/** Platforms that ship this page. Default: all. Others drop it from their tree and bundle. */
platforms?: PlatformName[];
/** Render mode on the web platform, overriding its `mode`. Native platforms always render on the client. */
render?: 'ssr' | 'client' | 'static';
/**
* The page's components, loaded on first visit: `() => import('#app/pages/home')`. The module's
* default export is the component, or `definePage({ component, pendingComponent, errorComponent })`.
*/
lazy?: () => Promise<{
default: RouteComponent | PageModule;
} | PageModule>;
}
How a page moves and lives inside a stack outlet, and where it ships. Lands in TanStack's staticData.
KitrRouterBindings
interface KitrRouterBindings {
session?: GuardSession | null;
platform?: GuardPlatform;
}
Fields createKitrRouter fills from the container when the packages are installed, as live
getters: session from @kitr/auth (what requireAuth() and can() read) and platform
from @kitr/platform (what onlyPlatform() reads). Each package augments KitrRouterContext
with its own precise type; this is their shape as the guards see it.
KitrRouterContext
interface KitrRouterContext {
/** The query client loaders fill with `ensureQueryData`. Per request on the server. */
queryClient: QueryClient;
/** The kitr app, forked per request on the server. Read services with `context.app.container.use()`. */
app: KitrApp;
}
The router context every kitr route sees. Packages extend it by augmentation:
declare module '@kitr/router' { interface KitrRouterContext { session: Session | null } }
OnlyPlatformOptions
interface OnlyPlatformOptions {
/** Redirect other platforms here instead of rendering not-found. */
to?: string;
}
Options of onlyPlatform(): where other platforms go instead of a 404.
PageModule
interface PageModule {
component?: RouteComponent;
pendingComponent?: RouteComponent;
errorComponent?: ErrorRouteComponent;
notFoundComponent?: NotFoundRouteComponent;
}
What a page module may export as default besides a bare component.
PageRoutes
interface PageRoutes<R = any> {
$types: R;
compile(target: 'tanstack', options?: CompileTreeOptions): Record<string, unknown>;
}
A route table with pages (defineRoutes([pages], ...)): what createKitrRouter({ routes }) compiles.
PageRouteType
interface PageRouteType<Rt extends AnyRoute, Kind extends string = 'page'> {
kind: Kind;
scope: 'absolute';
path: Rt['fullPath'];
/** The TanStack route, exactly as `createRoute` types it. */
route: Rt;
}
The entry type carried per page into $types. scope: 'absolute': groups never rewrite it.
RequireAuthOptions
interface RequireAuthOptions {
/** Where to send anonymous users. Default: `signInPath` from `config/auth.ts`, else `/sign-in`. */
to?: string;
/** Name of the search param carrying the original URL. Default `redirect`. */
redirectParam?: string | false;
}
Options of requireAuth(): where anonymous visitors go and which search param remembers their page.
RootComponentOptions
interface RootComponentOptions {
/** Rendered when no page matches the path, and for `notFound()` thrown without a closer handler. */
notFoundComponent?: LazyComponent<NotFoundRouteComponent>;
/** Rendered when a loader or component throws, unless a page has its own. */
errorComponent?: LazyComponent<ErrorRouteComponent> | false | null;
/** Rendered while a page loads, past the router's `defaultPendingMs`. */
pendingComponent?: LazyComponent<RouteComponent>;
}
The root's fallback components, each a component or { lazy }, the way .page() takes lazy.
DefaultRootRoute
/** The root route `createKitrRouter` builds when the table has no `.root(...)`: a kitr context, no loader. */
type DefaultRootRoute = RootRoute<Register, undefined, KitrRouterContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>;
The root route createKitrRouter builds when the table has no .root(...): a kitr context, no loader.
GroupRouteType
type GroupRouteType<R, P extends string, O> = PageRouteType<GroupRoute<R, P, O>, 'page-group'>;
Reserved for the group route the pages extension registers under a group. Not a route type itself.
LazyComponent
type LazyComponent<C> = C | {
lazy: () => Promise<{
default: C;
}>;
};
A component, or { lazy: () => import('./pages/not-found') } to load it the first time it renders.
The module's default export is the component.
PageName
type PageName<P extends AnyRoute, Path extends string> = `page ${JoinPaths<TrimRight<P['fullPath']>, Path>}`;
The name of a page entry: page /admin/users, the full path without a trailing slash.
ParentOf
type ParentOf<R> = R extends {
'~parent': infer T extends AnyRoute;
} ? T : RootOf<R>;
The route pages added here become children of: the enclosing layout or group, else the root.
RootOf
type RootOf<R> = R extends {
'~root': infer T extends AnyRoute;
} ? T : DefaultRootRoute;
The table's root route type: from .root(...), else the default.
RoutePath
type RoutePath<T> = NoTrailingSlash<PagePaths<T extends {
$types: infer R;
} ? R : T> & string>;
Every page path of a route table, as <Link to> takes it: '/' | '/about' | '/docs/$slug'.
Type a nav list with it ({ to: RoutePath<typeof routes> }[]) instead of widening to string;
a dynamic page stays a template, with its value in params. Takes the table or its $types.
RouterContextWith
type RouterContextWith<T> = KitrRouterContext & T;
The router context plus your own fields, for typing a route's context without augmenting KitrRouterContext.
RouteTreeOf
type RouteTreeOf<R> = Subtree<R, RootOf<R>>;
The route tree compile('tanstack') returns for a table's $types: what createKitrRouter({ routes }) registers.
RouteUnder
type RouteUnder<TParent extends AnyRoute, TPath extends string, TCustomId extends string, TSearchValidator = undefined, TParams = ResolveParams<TPath>, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, TSSR = unknown> = Route<unknown, TParent, TPath, ResolveFullPath<TParent, TPath>, TCustomId, ResolveId<TParent, TCustomId, TPath>, TSearchValidator, TParams, AnyContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, unknown, unknown, TSSR, unknown, undefined>;
A TanStack route under TParent, as createRoute({ getParentRoute, path | id, ...options }) types it.
TreeOf
type TreeOf<R> = RouteTreeOf<R> extends (infer T extends AnyRoute) ? T : never;
The TanStack route tree of a table's $types, as a route (never unknown, so the router stays typed).
WithChildren
type WithChildren<Rt, C> = IsAny<Rt extends {
types: {
parentRoute: infer P;
};
} ? P : never> extends true ? Rt extends RootRoute<infer TRegister, infer TSearchValidator, infer TRouterContext, infer TRouteContextFn, infer TBeforeLoadFn, infer TLoaderDeps extends Record<string, any>, infer TLoaderFn, any, infer TFileRouteTypes, infer TSSR, infer TServerMiddlewares, infer THandlers> ? RootRoute<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, C, TFileRouteTypes, TSSR, TServerMiddlewares, THandlers> : never : Rt extends Route<infer TRegister, infer TParentRoute extends AnyRoute, infer TPath extends string, infer TFullPath extends string, infer TCustomId extends string, infer TId extends string, infer TSearchValidator, infer TParams, infer TRouterContext, infer TRouteContextFn, infer TBeforeLoadFn, infer TLoaderDeps extends Record<string, any>, infer TLoaderFn, any, infer TFileRouteTypes, infer TSSR, infer TServerMiddlewares, infer THandlers> ? Route<TRegister, TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, C, TFileRouteTypes, TSSR, TServerMiddlewares, THandlers> : never;
Rt with its TChildren replaced by C, for a root or a child route.
@kitr/router/routes
compileTree
function compileTree(entries: RouteEntry[], options?: CompileTreeOptions): AnyRoute;
Build the TanStack route tree from the table's page entries.
definePage
function definePage(page: PageModule): PageModule;
export default definePage({ component: HomePage, pendingComponent: HomeSkeleton }) in a page file.
pages
const pages: {
readonly key: "pages";
readonly kinds: readonly [
"page",
"page-layout",
"page-group",
"page-root"
];
readonly methods: {
readonly root: (this: AnyRouteTable, options: Record<string, unknown>) => AnyRouteTable;
readonly page: (this: AnyRouteTable, path: string, options?: Record<string, unknown>) => AnyRouteTable;
readonly layout: (this: AnyRouteTable, id: string, ...rest: unknown[]) => AnyRouteTable;
};
readonly group: (child: AnyRouteTable, parent: AnyRouteTable, prefix: string, options: GroupOptions) => void;
readonly targets: {
readonly tanstack: typeof compileTree;
};
};
The pages extension for the kitr route table: .root, .page and .layout on
defineRoutes([pages], ...), compiled to a TanStack Router tree by createKitrRouter({ routes }).
import pages from '@kitr/router/routes'
export const routes = defineRoutes([pages], (r) =>
r.root({ head, component: RootDocument }).page('/', { lazy: () => import('./pages/home') }),
)
CompileTreeOptions
interface CompileTreeOptions {
/** The current platform name; pages whose `platforms` leave it out are dropped. */
platform?: string;
/** Default root route options when the table has no `.root(...)`. */
root?: Record<string, unknown>;
}
What compile('tanstack', options) receives from createKitrRouter.
KitrPageOptions
interface KitrPageOptions {
/** How the page enters in a stack outlet: slide in (`push`, default), slide up over the parent (`cover`), bottom sheet, or full screen. */
presentation?: 'push' | 'cover' | 'sheet' | 'fullscreen';
/** Keep the page mounted (state, scroll, media) when another page covers it. */
keepAlive?: boolean;
/** The page a cold deep link stacks under this one (`/chats/$id` under `/chats`). */
stackParent?: string;
/** Platforms that ship this page. Default: all. Others drop it from their tree and bundle. */
platforms?: PlatformName[];
/** Render mode on the web platform, overriding its `mode`. Native platforms always render on the client. */
render?: 'ssr' | 'client' | 'static';
/**
* The page's components, loaded on first visit: `() => import('#app/pages/home')`. The module's
* default export is the component, or `definePage({ component, pendingComponent, errorComponent })`.
*/
lazy?: () => Promise<{
default: RouteComponent | PageModule;
} | PageModule>;
}
How a page moves and lives inside a stack outlet, and where it ships. Lands in TanStack's staticData.
PageModule
interface PageModule {
component?: RouteComponent;
pendingComponent?: RouteComponent;
errorComponent?: ErrorRouteComponent;
notFoundComponent?: NotFoundRouteComponent;
}
What a page module may export as default besides a bare component.
PageRouteType
interface PageRouteType<Rt extends AnyRoute, Kind extends string = 'page'> {
kind: Kind;
scope: 'absolute';
path: Rt['fullPath'];
/** The TanStack route, exactly as `createRoute` types it. */
route: Rt;
}
The entry type carried per page into $types. scope: 'absolute': groups never rewrite it.
RootComponentOptions
interface RootComponentOptions {
/** Rendered when no page matches the path, and for `notFound()` thrown without a closer handler. */
notFoundComponent?: LazyComponent<NotFoundRouteComponent>;
/** Rendered when a loader or component throws, unless a page has its own. */
errorComponent?: LazyComponent<ErrorRouteComponent> | false | null;
/** Rendered while a page loads, past the router's `defaultPendingMs`. */
pendingComponent?: LazyComponent<RouteComponent>;
}
The root's fallback components, each a component or { lazy }, the way .page() takes lazy.
DefaultRootRoute
/** The root route `createKitrRouter` builds when the table has no `.root(...)`: a kitr context, no loader. */
type DefaultRootRoute = RootRoute<Register, undefined, KitrRouterContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>;
The root route createKitrRouter builds when the table has no .root(...): a kitr context, no loader.
GroupRouteType
type GroupRouteType<R, P extends string, O> = PageRouteType<GroupRoute<R, P, O>, 'page-group'>;
Reserved for the group route the pages extension registers under a group. Not a route type itself.
LazyComponent
type LazyComponent<C> = C | {
lazy: () => Promise<{
default: C;
}>;
};
A component, or { lazy: () => import('./pages/not-found') } to load it the first time it renders.
The module's default export is the component.
PageName
type PageName<P extends AnyRoute, Path extends string> = `page ${JoinPaths<TrimRight<P['fullPath']>, Path>}`;
The name of a page entry: page /admin/users, the full path without a trailing slash.
ParentOf
type ParentOf<R> = R extends {
'~parent': infer T extends AnyRoute;
} ? T : RootOf<R>;
The route pages added here become children of: the enclosing layout or group, else the root.
RootOf
type RootOf<R> = R extends {
'~root': infer T extends AnyRoute;
} ? T : DefaultRootRoute;
The table's root route type: from .root(...), else the default.
RoutePath
type RoutePath<T> = NoTrailingSlash<PagePaths<T extends {
$types: infer R;
} ? R : T> & string>;
Every page path of a route table, as <Link to> takes it: '/' | '/about' | '/docs/$slug'.
Type a nav list with it ({ to: RoutePath<typeof routes> }[]) instead of widening to string;
a dynamic page stays a template, with its value in params. Takes the table or its $types.
RouteTreeOf
type RouteTreeOf<R> = Subtree<R, RootOf<R>>;
The route tree compile('tanstack') returns for a table's $types: what createKitrRouter({ routes }) registers.
RouteUnder
type RouteUnder<TParent extends AnyRoute, TPath extends string, TCustomId extends string, TSearchValidator = undefined, TParams = ResolveParams<TPath>, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, TSSR = unknown> = Route<unknown, TParent, TPath, ResolveFullPath<TParent, TPath>, TCustomId, ResolveId<TParent, TCustomId, TPath>, TSearchValidator, TParams, AnyContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, unknown, unknown, TSSR, unknown, undefined>;
A TanStack route under TParent, as createRoute({ getParentRoute, path | id, ...options }) types it.
WithChildren
type WithChildren<Rt, C> = IsAny<Rt extends {
types: {
parentRoute: infer P;
};
} ? P : never> extends true ? Rt extends RootRoute<infer TRegister, infer TSearchValidator, infer TRouterContext, infer TRouteContextFn, infer TBeforeLoadFn, infer TLoaderDeps extends Record<string, any>, infer TLoaderFn, any, infer TFileRouteTypes, infer TSSR, infer TServerMiddlewares, infer THandlers> ? RootRoute<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, C, TFileRouteTypes, TSSR, TServerMiddlewares, THandlers> : never : Rt extends Route<infer TRegister, infer TParentRoute extends AnyRoute, infer TPath extends string, infer TFullPath extends string, infer TCustomId extends string, infer TId extends string, infer TSearchValidator, infer TParams, infer TRouterContext, infer TRouteContextFn, infer TBeforeLoadFn, infer TLoaderDeps extends Record<string, any>, infer TLoaderFn, any, infer TFileRouteTypes, infer TSSR, infer TServerMiddlewares, infer THandlers> ? Route<TRegister, TParentRoute, TPath, TFullPath, TCustomId, TId, TSearchValidator, TParams, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, C, TFileRouteTypes, TSSR, TServerMiddlewares, THandlers> : never;
Rt with its TChildren replaced by C, for a root or a child route.
@kitr/router/stack
animate
function animate(element: Element | null | undefined, keyframes: Keyframe[], options: AnimateOptions): Promise<void>;
Animate an element and resolve when done. Resolves at once without the Web Animations API or with no keyframes.
attachDrag
function attachDrag(element: HTMLElement, options: DragGestureOptions): () => void;
Attach a drag gesture to a page element. Returns the detach function.
createBackRegistry
function createBackRegistry(): BackRegistry;
A new registry; <StackOutlet> makes one per stack and useBackHandler() registers into the nearest.
createEntryRouter
function createEntryRouter(router: AnyRouter, ancestorIds: string[], initial: {
matches: AnyRouteMatch[];
location: ParsedLocation;
}): EntryRouter;
Build the router view for one stack page. ancestorIds are the routes above the outlet, shared live with the real router.
detectMotionStyle
function detectMotionStyle(win?: Window | undefined): MotionStyle;
The style for the device: iOS and Android from the user agent, everything else plain. Reduced motion → none.
motionFor
function motionFor(presentation: Presentation, style: MotionStyle): MotionStep;
The motion for a presentation in a style. none keeps the page but skips the animation.
shouldCommit
function shouldCommit(distance: number, size: number, velocity: number, rule?: GestureRule): boolean;
Whether a release at distance of size pixels, moving at velocity px/s, pops the page.
SplitStack
function SplitStack({ master, children, minWidth, masterWidth, className, style }: SplitStackProps): import("react").JSX.Element;
The list beside the stack above minWidth, the stack alone below it.
stackOptionsOf
function stackOptionsOf(pageMatches: AnyRouteMatch[]): StackRouteOptions;
The stack options of a page: the innermost route's staticData wins, then its parents below the outlet.
StackOutlet
function StackOutlet(props: StackOutletProps): React.JSX.Element;
The stacked outlet. A plain <Outlet> on the server and until hydration, then pages that push, pop and stay mounted beneath.
StackTabs
function StackTabs({ tabs, children, placement, backToHome, className, style, ...outlet }: StackTabsProps): import("react").JSX.Element;
<StackTabs>: tabs that each keep their own stack. Switching tabs shows where that tab was
left; tapping the current tab again pops it to its root; back inside a tab pops that tab's
pages. Routes decide which tab they belong to by path (match), so the same route tree serves
a tabbed phone app and a sidebar desktop app.
useBackHandler
function useBackHandler(handler: BackHandler, enabled?: boolean): void;
Answer the back button while this component is mounted: a dialog closes itself, a form asks
before leaving. Return true to stop the stack from popping. The newest handler runs first.
useMinWidth
function useMinWidth(minWidth: number): boolean;
true once the viewport is at least minWidth wide. Hydration-safe: false on the server and first render.
useSplitPane
function useSplitPane(): SplitPane;
Whether the nearest <SplitStack> shows both panes. false outside one, on the server and before hydration.
useStack
function useStack(): StackInfo;
The stack around the current page: depth, whether it can pop, and pop().
useStackEntry
function useStackEntry(): StackEntry | undefined;
The entry of the page this component renders in, or undefined outside a stack page.
StackEngine
class StackEngine {
entries: StackEntry[];
activeId: string | undefined;
transition: StackTransition | undefined;
private listeners;
private fallbackIndex;
/** Set by the tabs view before navigating to a tab's last page: that push reuses the page instead of stacking a copy. */
switchingTo: string | undefined;
/** Set by a completed swipe gesture: the next pop needs no leave animation. */
gesturePopped: string | undefined;
readonly options: Required<StackEngineOptions>;
constructor(options?: StackEngineOptions);
subscribe: (listener: () => void) => () => void;
private notify;
get active(): StackEntry | undefined;
byId(id: string): StackEntry | undefined;
/** The pages of one group that are not leaving, bottom first. */
stackOf(group: string): StackEntry[];
/** The topmost page of a group that is not leaving. */
topOf(group: string): StackEntry | undefined;
/** How many pages the active group holds (what "can go back" means to the stack). */
get depth(): number;
/** Whether a page should be in the tree: entering, active, leaving, or covered and kept alive within the limit. */
isMounted(entry: StackEntry): boolean;
/** Apply a committed router state. Returns the transition to show, or undefined when the current page only refreshed. */
apply(input: ApplyInput): StackTransition | undefined;
private create;
private refresh;
private begin;
/** The view finished animating a transition: entering pages are active, leaving pages are gone. */
settle(transitionId: number): void;
/** Remove a page whose leave animation ended. */
finishLeaving(id: string, notify?: boolean): void;
/** Drop every page (the outlet unmounted). */
reset(): void;
}
The stack's state machine, free of React and the DOM so it can be tested on its own. Feed it
every committed router state with apply(); it decides whether that was a push, a pop, a
replace or a tab switch, keeps the pages in order with their last matches, and tells the view
what to animate. The view reports the end of leave animations with finishLeaving().
defaultBackRegistry
const defaultBackRegistry: BackRegistry;
One registry for the app: <StackOutlet> reads it, and so does useBackHandler() outside any stack.
AnimateOptions
interface AnimateOptions {
duration: number;
easing: string;
/** Play the keyframes backwards (a leaving page). */
reverse?: boolean;
/** Start part-way (a gesture already dragged the page this far), 0..1. */
from?: number;
}
Duration and easing for animate(), with reverse for a leaving page and from to continue a gesture.
ApplyInput
interface ApplyInput {
/** The live router's matches, root first. */
matches: AnyRouteMatch[];
location: ParsedLocation;
/** The id of the route whose outlet this is; pages are the matches below it. */
outletRouteId: string;
}
A committed router state for StackEngine.apply(): the matches, the location and the outlet's route.
BackRegistry
interface BackRegistry {
push(handler: BackHandler): () => void;
/** Run the handlers newest first; true when one consumed the press. */
run(): boolean;
readonly size: number;
}
The handlers that answer the back button, newest first.
DragGestureOptions
interface DragGestureOptions extends GestureRule {
/** `x`: horizontal from the left edge (swipe back). `y`: downward from anywhere (dismiss). */
axis: 'x' | 'y';
/** Start only at the leading edge (swipe back) or anywhere (dismiss). */
fromEdge: boolean;
/** Progress 0..1 as the pointer moves. */
onProgress(progress: number): void;
/** The pointer lifted: `commit` says whether to pop; `progress` is where it stopped. */
onEnd(commit: boolean, progress: number): void;
/** Called when the gesture claims the pointer (after `slop`). */
onStart?(): void;
/** Whether a gesture may start now (not mid-transition, not at the root). */
enabled?(): boolean;
}
What attachDrag() needs: the axis, whether the drag starts at the edge, and the progress callbacks.
EntryRouter
interface EntryRouter {
/** The view of the router to put in `<RouterContextProvider>` around the page. */
router: AnyRouter;
stores: EntryStores;
/** Mirror the live router into this page: called while the page is the current location. */
update(matches: AnyRouteMatch[], location: ParsedLocation, status: RouterStatus): void;
}
A page's view of the router: the proxy to render through, its stores, and update() to mirror the live router while the page is current.
GestureRule
interface GestureRule {
/** Pointers must start within this many pixels of the edge (swipe back). Default 24. */
edge?: number;
/** Movement needed before the gesture claims the pointer. Default 8. */
slop?: number;
/** Fraction of the page that commits a release with no speed. Default 0.3. */
commitAt?: number;
/** Velocity (px/s) that commits regardless of distance. Default 800. */
velocity?: number;
}
Pointer gestures for the stack: the iOS edge swipe that pops a pushed page, and the drag that dismisses a cover or a sheet. Pure DOM so the rules can be tested without React.
MotionStep
interface MotionStep {
/** Keyframes for the page coming to the front. */
enter: Keyframe[];
/** Keyframes for the page leaving the front (the reverse of `enter` when omitted). */
exit?: Keyframe[];
/** Keyframes for the page beneath while another arrives over it. Reversed when that page leaves. */
under?: Keyframe[];
/** Whether the page beneath is dimmed by a scrim. */
scrim: boolean;
duration: number;
easing: string;
}
The keyframes of one presentation in one style: for the arriving page, the leaving page and the page beneath.
SplitPane
interface SplitPane {
/** Whether both panes show. */
split: boolean;
}
What useSplitPane() returns: whether the list pane shows beside the stack.
SplitStackProps
interface SplitStackProps {
/** The list (master) pane. */
master: ReactNode;
/** The detail pane: a `<StackOutlet>` or `<Outlet>`. */
children: ReactNode;
/** Split at this viewport width in pixels. Default 768. */
minWidth?: number;
/** The master pane's width. Default `320px`. */
masterWidth?: string | number;
className?: string;
style?: CSSProperties;
}
<SplitStack>: a list beside the stack when the window is wide enough, the stack alone when it
is not. Phones push the detail over the list; tablets and desktops show both. The list route
reads useSplitPane() to know whether it is the whole screen (a page in the stack) or the
left pane (and the index route shows a placeholder in the right pane).
StackEngineOptions
interface StackEngineOptions {
/** How many covered pages stay mounted per group. `false` keeps none (every page remounts on back). Default 5. */
keepAlive?: number | false;
/** The tab a location belongs to. Default: one group. */
groupOf?: (location: ParsedLocation, matches: AnyRouteMatch[]) => string;
}
Options of StackEngine and <StackOutlet>: the keep-alive limit and how locations group into tabs.
StackEntry
interface StackEntry {
/** Stable, unique per page instance: React's key. */
id: string;
/** The history entry key (`location.state.key`) of this page's latest visit. */
key: string;
/** The history index (`location.state.__TSR_index`) of that visit. */
index: number;
href: string;
pathname: string;
/** The route ids below the outlet's route, outermost first. */
routeIds: string[];
presentation: Presentation;
/** Whether the page stays mounted under later pages. */
keepAlive: boolean;
/** The tab (or `'default'`) the page belongs to. */
group: string;
phase: EntryPhase;
/** The page's matches at its last visit, outlet route excluded, with the ancestors in front. */
matches: AnyRouteMatch[];
location: ParsedLocation;
}
One page in the stack: its history key and index, route ids, presentation and last matches.
StackInfo
interface StackInfo {
/** The active page's entry, or undefined outside a stack. */
entry: StackEntry | undefined;
/** How many pages the current tab holds. */
depth: number;
/** Whether `pop()` has a page to go back to. */
canPop: boolean;
/** Go back one page (the router's history back). */
pop(): void;
/** Dismiss the top page the way its presentation suggests: the same as `pop()` for now. */
dismiss(): void;
presentation: Presentation | undefined;
/** Register a handler for the back button, newest first. */
registry: BackRegistry;
}
What useStack() returns inside a stacked page.
StackOutletProps
interface StackOutletProps extends StackEngineOptions {
/** `ios`, `android`, `plain` (web, desktop) or `none`. Default: detected from the device, `none` with reduced motion. */
motion?: MotionStyle | 'auto';
/** Edge swipe back and drag to dismiss. Default: on for touch input. */
gestures?: boolean | GestureRule;
/** Back pressed at the root of the stack (Android back with nothing to pop). Return true when handled. Default: exit the app on Android, else nothing. */
onRootBack?: () => boolean | undefined;
/** An engine shared with `<StackTabs>`; one is created otherwise. */
engine?: StackEngine;
className?: string;
style?: CSSProperties;
}
<StackOutlet>: an <Outlet> whose pages stack. A navigation deeper pushes the new page over
the current one, which stays mounted (its state, scroll position and media untouched); back
slides it away and the page beneath is there as it was. Routes declare how they arrive
(staticData.presentation: push, cover, sheet, fullscreen); iOS gets edge swipe back, Android
the hardware back button, and a cold start on a deep page gets its stackParent beneath it.
On the server and during hydration it renders a plain <Outlet>, so SSR output and the first
client paint are the ordinary route tree; the stack takes over after hydration.
StackRouteOptions
interface StackRouteOptions {
/** How the page arrives. Default `push`. */
presentation?: Presentation;
/** Keep this page mounted (state, scroll, media) under later pages. Default: the outlet's `keepAlive`. */
keepAlive?: boolean;
/**
* The page a cold start lands under: opening `/chats/5` from a notification puts `/chats` beneath
* it so swipe back and the back button have somewhere to go. A pathname (`/chats`).
*/
stackParent?: string;
}
Route options the stack reads from staticData. Declare them on a route:
export const Route = createFileRoute('/post/new')({ staticData: { presentation: 'cover' } })
StackTab
interface StackTab {
/** Stable id, also the stack group. */
id: string;
/** The path the tab opens the first time. */
to: string;
label?: ReactNode;
icon?: ReactNode;
/** Paths that belong to this tab. Default: `to` and anything under it. */
match?: string[] | ((pathname: string) => boolean);
}
One tab: where it starts and which paths belong to it.
StackTabsProps
interface StackTabsProps extends Omit<StackOutletProps, 'groupOf' | 'engine' | 'onRootBack'> {
tabs: StackTab[];
/** The tab bar. Receives the tabs, the current one and `select()`. Rendered after the pages. */
children: (props: StackTabsRenderProps) => ReactNode;
/** Where the bar goes. Default `bottom` (`top` for a desktop sidebar is yours to style). */
placement?: 'top' | 'bottom';
/**
* Android back at the root of a tab other than the first goes to the first tab before exiting
* (Material's "home tab" rule). Default true.
*/
backToHome?: boolean;
className?: string;
style?: CSSProperties;
}
Props of <StackTabs>: the tabs, the bar as a render prop, and the outlet's own options.
StackTabsRenderProps
interface StackTabsRenderProps {
tabs: StackTab[];
/** The tab whose page shows. */
current: StackTab | undefined;
/** Open a tab: its last page, or its root when it is already current. */
select(tab: StackTab | string): void;
/** The tab a pathname belongs to. */
tabOf(pathname: string): StackTab | undefined;
}
What the tab bar render prop receives: the tabs, the current one, select() and tabOf().
StackTransition
interface StackTransition {
id: number;
kind: TransitionKind;
/** The page coming to the front. */
to: string;
/** The page it replaces at the front, if any. */
from?: string;
/** The pages leaving the stack; removed once `finishLeaving()` is called for them. */
leaving: string[];
/** Whether the group (tab) changed. */
groupChanged: boolean;
/** False when the move was already shown (a completed swipe gesture) and needs no animation. */
animate: boolean;
}
One move the view animates: what comes to the front, what leaves, and how.
BackHandler
type BackHandler = () => boolean | undefined;
Who answers the back button. Handlers register last-in-first-out: an open menu or dialog
registers one while it shows, so Android back (and a desktop window's back key) closes it
before the stack pops a page. A handler returns true when it consumed the press.
EntryPhase
type EntryPhase = 'entering' | 'active' | 'covered' | 'leaving';
Where a page is in its life: arriving, in front, under another page, or on its way out.
MotionStyle
type MotionStyle = 'ios' | 'android' | 'plain' | 'none';
The look of a transition: iOS slides the new page in from the right with the page beneath drifting left, Android fades it in while it rises a little, the web and desktops cross-fade. Covers and sheets rise from the bottom everywhere. Every animation runs through the Web Animations API; where it is missing (an old WebView, happy-dom) the transition settles at once.
Presentation
type Presentation = 'push' | 'cover' | 'sheet' | 'fullscreen';
How a page arrives when it is pushed on the stack.
push: slides in from the side; the page below stays mounted and slides back out on pop.cover: rises from the bottom over the whole screen, dismissed by dragging down (a composer, a player).sheet: rises from the bottom over a dimmed page that stays visible (a picker, a menu).fullscreen: likepushbut with no swipe-back gesture (a video, a camera).
TransitionKind
type TransitionKind = 'push' | 'pop' | 'replace' | 'switch';