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 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 JSX element that produces a Flutter Page object. The built-in <materialPage> creates a MaterialPage — but you can create your own for Cupertino transitions, custom animations, or any other page behavior.

FusePage base class

abstract class FusePage {
  FusePage(this.node);
  final FuseNode node;

  /// Reactive child content — rebuilds when signals change
  /// without rebuilding the page transition.
  Widget get child => ListenableBuilder(
    listenable: node,
    builder: (_, _) => node.flexChildren,
  );

  /// Build the Flutter Page for the Navigator.
  Page build();
}

The child getter is reactive — it wraps node.flexChildren in a ListenableBuilder, so page content updates when signals change without re-triggering the page transition animation.

Example: CupertinoPage

dart/lib/src/routes/cupertino_page.dart
import 'package:flutter/cupertino.dart';
import 'package:solid_fuse/solid_fuse.dart';

class FuseCupertinoPage extends FusePage {
  FuseCupertinoPage(super.node);

  @override
  Page build() => CupertinoPage(
    key: ValueKey(node.id),
    title: node.string('title'),
    child: child,
  );
}

Register it:

runtime.registerPage('cupertinoPage', FuseCupertinoPage.new);

Declare the JSX type:

src/cupertino-page.d.ts
import type { FlexInput } from "solid-fuse";

declare global {
  namespace JSX {
    interface IntrinsicElements {
      cupertinoPage: {
        title?: string;
        children?: any;
        flex?: FlexInput;
      };
    }
  }
}

Use it:

function SettingsPage() {
  return (
    <cupertinoPage title="Settings" flex={{ gap: 16 }}>
      <text>Settings content</text>
    </cupertinoPage>
  );
}

Example: Custom fade transition

dart/lib/src/routes/fade_page.dart
import 'package:flutter/material.dart';
import 'package:solid_fuse/solid_fuse.dart';

class FuseFadePage extends FusePage {
  FuseFadePage(super.node);

  @override
  Page build() => _FadePageRoute(
    key: ValueKey(node.id),
    duration: Duration(
      milliseconds: node.int('duration') ?? 300,
    ),
    child: child,
  );
}

class _FadePageRoute extends Page {
  const _FadePageRoute({
    super.key,
    required this.duration,
    required this.child,
  });

  final Duration duration;
  final Widget child;

  @override
  Route createRoute(BuildContext context) {
    return PageRouteBuilder(
      settings: this,
      transitionDuration: duration,
      pageBuilder: (_, __, ___) => child,
      transitionsBuilder: (_, animation, __, child) {
        return FadeTransition(opacity: animation, child: child);
      },
    );
  }
}

Fallback behavior

If a navigator encounters a child element without a registered page factory, it wraps it in a plain MaterialPage automatically. So you can push non-page elements and they still work — they just get the default Material transition.

// This works even without a page wrapper
nav.push(() => (
  <view flex={{ align: "center", justify: "center", expand: true }}>
    <text>Plain view as a page</text>
  </view>
));

Key points

  • Pages are created on-demand for each navigator child, not cached
  • The child getter is reactive — content updates don't re-trigger transitions
  • Always use ValueKey(node.id) as the page key for identity stability
  • Pages support flex via flexChildren — layout props work on page elements just like any other

On this page