Creating Pages
Custom navigation transitions and page types
Pages define both the content and the transition for navigation. When you push a page, the page type itself decides how it animates in and out. This page covers creating custom page types beyond the built-in materialPage.
How pages work
A page in Fuse is a configuration object — { type, child, props } — not a JSX element. Consumers build one with a factory like materialPage({...}) and pass it to nav.push(...). The <Navigator> component renders each entry by handing the configuration to a registered Dart page handle.
Page handles are just FuseHandle<Page> — same machinery as scroll controllers and focus nodes — but they extend FusePageHandle, which adds a stable key and a reactive content widget for Flutter's Navigator 2.0.
JS side: a factory function
import type { PageConfig } from "solid-fuse";
export type CupertinoPageProps = {
child: () => JSX.Element;
name?: string;
title?: string;
maintainState?: boolean;
};
export function cupertinoPage({
child,
...props
}: CupertinoPageProps): PageConfig<Omit<CupertinoPageProps, "child">> {
return { type: "cupertinoPage", child, props };
}The factory returns a PageConfig that the Navigator forwards to your Dart handle:
type— the registration key (matchesruntime.registerHandle('cupertinoPage', ...))child— a thunk so the page contents aren't built until the page is mountedprops— anything else lands on the node and is readable in Dart vianode.string(...)etc.
Consumers use it like any other page:
nav.push(cupertinoPage({ title: "Settings", child: () => <SettingsPage /> }));Dart side: FusePageHandle
FusePageHandle extends FuseHandle<Page> and gives you two helpers:
pageKey— aLocalKeyderived from the_pageIdprop the Navigator assigns to each entry. Use it askey:on yourPagesubclass so Navigator 2.0 can diff the stack correctly.pageContent— a reactive widget rendering the node's JSX children withflexChildrenlayout. Wrap it in your page type's scaffolding (Cupertino, Material, custom).
import 'package:flutter/cupertino.dart';
import 'package:solid_fuse/solid_fuse.dart';
class FuseCupertinoPage extends FusePageHandle {
FuseCupertinoPage(super.node);
@override
late final Page<dynamic> object = CupertinoPage<dynamic>(
key: pageKey,
name: node.string('name'),
title: node.string('title'),
maintainState: node.bool('maintainState') ?? true,
child: pageContent,
);
}Register it like any other handle:
runtime.registerHandle('cupertinoPage', FuseCupertinoPage.new);Example: custom fade transition
import 'package:flutter/material.dart';
import 'package:solid_fuse/solid_fuse.dart';
class FuseFadePage extends FusePageHandle {
FuseFadePage(super.node);
@override
late final Page<dynamic> object = _FadePage(
key: pageKey,
duration: Duration(milliseconds: node.int('duration') ?? 300),
child: pageContent,
);
}
class _FadePage extends Page<dynamic> {
const _FadePage({
super.key,
required this.duration,
required this.child,
});
final Duration duration;
final Widget child;
@override
Route<dynamic> createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
transitionDuration: duration,
pageBuilder: (_, _, _) => child,
transitionsBuilder: (_, animation, _, child) {
return FadeTransition(opacity: animation, child: child);
},
);
}
}The matching JS factory:
export function fadePage(opts: { child: () => JSX.Element; duration?: number }) {
const { child, ...props } = opts;
return { type: "fadePage", child, props };
}Fallback behavior
If the Navigator encounters a page config whose type isn't registered, it falls back to wrapping the child in a plain MaterialPage. That means nav.push(() => <X />) works even before you've defined any custom page types — the thunk is sugar for materialPage({ child }), and any unrecognized type still renders something visible.
Key points
- Pages are factory functions producing
PageConfigobjects, not JSX elements - Page handles extend
FusePageHandleso they pick uppageKeyandpageContent - Always use
pageKey(not a manualValueKey) so Navigator 2.0's page diff stays stable pageContentis reactive — content updates don't re-trigger transitions- Register via
runtime.registerHandle(name, FuseYourPage.new)like any other handle