solid-fuse

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

src/pages/cupertino.ts
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 (matches runtime.registerHandle('cupertinoPage', ...))
  • child — a thunk so the page contents aren't built until the page is mounted
  • props — anything else lands on the node and is readable in Dart via node.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 — a LocalKey derived from the _pageId prop the Navigator assigns to each entry. Use it as key: on your Page subclass so Navigator 2.0 can diff the stack correctly.
  • pageContent — a reactive widget rendering the node's JSX children with flexChildren layout. Wrap it in your page type's scaffolding (Cupertino, Material, custom).
dart/lib/src/handles/cupertino_page.dart
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

dart/lib/src/handles/fade_page.dart
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 PageConfig objects, not JSX elements
  • Page handles extend FusePageHandle so they pick up pageKey and pageContent
  • Always use pageKey (not a manual ValueKey) so Navigator 2.0's page diff stays stable
  • pageContent is reactive — content updates don't re-trigger transitions
  • Register via runtime.registerHandle(name, FuseYourPage.new) like any other handle

On this page