Flutter Motion Kit

Previewable Flutter animations + pitfall guide · one-click reuse from Claude Code

← Back

Custom page transition (fade + slide-up)

explicit · difficulty 2/5 · verified Flutter 3.32 / 2026-06

Use PageRouteBuilder to customize the enter animation (fade + a slight slide-up) instead of the default platform transition.

▶︎ Running for real (Flutter 3.32 · self-hosted, not a recording)

Code

// ✅ Recommended: PageRouteBuilder + CurvedAnimation, fade-in + slide-up, with explicit enter/exit durations.
// Paste straight into DartPad (https://dartpad.dev) to run.
import 'package:flutter/material.dart';

void main() => runApp(const _App());

class _App extends StatelessWidget {
  const _App();
  @override
  Widget build(BuildContext context) =>
      const MaterialApp(debugShowCheckedModeBanner: false, home: _HomePage());
}

// Reusable transition: fade-in + a slight slide-up
Route<T> fadeSlideRoute<T>(Widget page) {
  return PageRouteBuilder<T>(
    transitionDuration: const Duration(milliseconds: 350),
    reverseTransitionDuration: const Duration(milliseconds: 300),
    pageBuilder: (context, animation, secondaryAnimation) => page,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      final curved = CurvedAnimation(
        parent: animation,
        curve: Curves.easeOutCubic,
      );
      return FadeTransition(
        opacity: curved,
        child: SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(0, 0.08),
            end: Offset.zero,
          ).animate(curved),
          child: child, // reuse the passed-in child
        ),
      );
    },
  );
}

class _HomePage extends StatelessWidget {
  const _HomePage();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: FilledButton(
          onPressed: () =>
              Navigator.of(context).push(fadeSlideRoute(const _SecondPage())),
          child: const Text('Open with fade + slide'),
        ),
      ),
    );
  }
}

class _SecondPage extends StatelessWidget {
  const _SecondPage();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Second')),
      body: const Center(child: Text('Hello 👋')),
    );
  }
}

⚠️ Pitfalls (4)

Driving the Tween with a raw linear animation feels stiff.
✅ Wrap it in a CurvedAnimation (e.g. easeOutCubic) before driving Fade/Slide.
official-docs · source
Setting only transitionDuration and forgetting reverseTransitionDuration makes the back transition a different length.
✅ Set both durations explicitly.
official-docs · source
Rebuilding a big subtree inside transitionsBuilder instead of using the passed-in child rebuilds the page every frame.
✅ Always reuse the child argument from the callback.
official-docs · source
Hand-writing the same transition everywhere for an app-wide look is hard to maintain and inconsistent.
✅ Configure ThemeData.pageTransitionsTheme with a PageTransitionsBuilder to override it app-wide.
official-docs · source

Official docs