How do you implement a custom transition between screens in flutter?
#navigation#animation#transitions#page-route
Answer
Overview
Custom page transitions in Flutter allow you to create engaging navigation animations beyond the default platform transitions. Flutter provides
text
PageRouteBuilderCore Components
- PageRouteBuilder - Builds custom route transitions
- Animation
- Controls animation progress (0.0 to 1.0) - Tween - Defines animation start and end values
- Curve - Defines animation timing (easing)
- Transition Widgets - Apply animations (SlideTransition, FadeTransition, etc.)
Method 1: Using PageRouteBuilder
Basic Structure
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { // Define your custom transition here return child; // Replace with animated widget }, ), );
Animation Parameters
- animation - Primary animation (0.0 → 1.0 when pushing, 1.0 → 0.0 when popping)
- secondaryAnimation - Animation of route below (for exit animations)
- child - The destination screen (pageBuilder result)
Common Transition Types
1. Slide Transition
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1.0, 0.0); // Start from right const end = Offset.zero; // End at current position const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); return SlideTransition( position: animation.drive(tween), child: child, ); }, ), );
Slide Directions
dart// Slide from right const begin = Offset(1.0, 0.0); // Slide from left const begin = Offset(-1.0, 0.0); // Slide from bottom const begin = Offset(0.0, 1.0); // Slide from top const begin = Offset(0.0, -1.0); // Diagonal slide (bottom-right) const begin = Offset(1.0, 1.0);
2. Fade Transition
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: animation, child: child, ); }, ), );
3. Scale Transition
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return ScaleTransition( scale: animation, child: child, ); }, ), );
4. Rotation Transition
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return RotationTransition( turns: animation, child: child, ); }, ), );
5. Combined Transitions
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(0.0, 1.0); const end = Offset.zero; const curve = Curves.easeInOut; var slideTween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); return SlideTransition( position: animation.drive(slideTween), child: FadeTransition( opacity: animation, child: child, ), ); }, ), );
Advanced Transitions
1. Custom Duration
dartNavigator.push( context, PageRouteBuilder( transitionDuration: Duration(milliseconds: 800), reverseTransitionDuration: Duration(milliseconds: 400), pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); }, ), );
2. Shared Axis Transition (Material Design)
dartimport 'package:animations/animations.dart'; Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return SharedAxisTransition( animation: animation, secondaryAnimation: secondaryAnimation, transitionType: SharedAxisTransitionType.horizontal, child: child, ); }, ), );
3. Size Transition (Expandable)
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return Align( child: SizeTransition( sizeFactor: animation, child: child, ), ); }, ), );
4. Custom Curved Animation
dartNavigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { var curvedAnimation = CurvedAnimation( parent: animation, curve: Curves.elasticOut, reverseCurve: Curves.easeIn, ); return ScaleTransition( scale: curvedAnimation, child: child, ); }, ), );
Method 2: Custom PageRoute Class
Create reusable transition classes:
dartclass SlideRightRoute extends PageRouteBuilder { final Widget page; SlideRightRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1.0, 0.0); const end = Offset.zero; const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end); var curvedAnimation = CurvedAnimation( parent: animation, curve: curve, ); return SlideTransition( position: tween.animate(curvedAnimation), child: child, ); }, ); } // Usage Navigator.push( context, SlideRightRoute(page: DetailScreen()), );
More Custom Route Classes
dart// Fade Route class FadeRoute extends PageRouteBuilder { final Widget page; FadeRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); }, ); } // Scale Rotate Route class ScaleRotateRoute extends PageRouteBuilder { final Widget page; ScaleRotateRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return ScaleTransition( scale: Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: animation, curve: Curves.fastOutSlowIn, ), ), child: RotationTransition( turns: Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: animation, curve: Curves.linear, ), ), child: child, ), ); }, ); }
Method 3: Using go_router
dartimport 'package:go_router/go_router.dart'; final router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), ), GoRoute( path: '/details', pageBuilder: (context, state) => CustomTransitionPage( child: DetailScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1.0, 0.0); const end = Offset.zero; const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); return SlideTransition( position: animation.drive(tween), child: child, ); }, ), ), ], );
Complete Example with Multiple Transitions
dartimport 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Custom Transitions Demo', home: HomeScreen(), ); } } class HomeScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Custom Transitions')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () => Navigator.push( context, SlideRightRoute(page: DetailScreen(title: 'Slide Right')), ), child: Text('Slide Right'), ), SizedBox(height: 10), ElevatedButton( onPressed: () => Navigator.push( context, FadeRoute(page: DetailScreen(title: 'Fade')), ), child: Text('Fade'), ), SizedBox(height: 10), ElevatedButton( onPressed: () => Navigator.push( context, ScaleRoute(page: DetailScreen(title: 'Scale')), ), child: Text('Scale'), ), SizedBox(height: 10), ElevatedButton( onPressed: () => Navigator.push( context, CombinedRoute(page: DetailScreen(title: 'Combined')), ), child: Text('Slide + Fade'), ), ], ), ), ); } } class DetailScreen extends StatelessWidget { final String title; const DetailScreen({Key? key, required this.title}) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: Center( child: ElevatedButton( onPressed: () => Navigator.pop(context), child: Text('Go Back'), ), ), ); } } // Custom Route Classes class SlideRightRoute extends PageRouteBuilder { final Widget page; SlideRightRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1.0, 0.0); const end = Offset.zero; const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); return SlideTransition( position: animation.drive(tween), child: child, ); }, ); } class FadeRoute extends PageRouteBuilder { final Widget page; FadeRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); }, ); } class ScaleRoute extends PageRouteBuilder { final Widget page; ScaleRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return ScaleTransition( scale: Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: animation, curve: Curves.fastOutSlowIn, ), ), child: child, ); }, ); } class CombinedRoute extends PageRouteBuilder { final Widget page; CombinedRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(0.0, 1.0); const end = Offset.zero; const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end).chain( CurveTween(curve: curve), ); return SlideTransition( position: animation.drive(tween), child: FadeTransition( opacity: animation, child: child, ), ); }, ); }
Available Curves
dart// Common curves Curves.linear // Constant speed Curves.easeIn // Slow start Curves.easeOut // Slow end Curves.easeInOut // Slow start and end Curves.fastOutSlowIn // Material Design standard Curves.bounceIn // Bounce at start Curves.bounceOut // Bounce at end Curves.elasticIn // Elastic/spring start Curves.elasticOut // Elastic/spring end
Best Practices
- Keep duration reasonable (200-500ms for most transitions)
- Use platform-appropriate transitions (slide for iOS, fade for Android)
- Don't overuse complex animations - they can slow UX
- Match transition direction to user gesture (swipe right = slide left)
- Test on real devices - animations can lag on slower phones
- Reuse route classes instead of duplicating transition code
- Consider accessibility - some users may prefer reduced motion
Common Use Cases
| Transition | Best For |
|---|---|
| Slide | Modal sheets, detail views |
| Fade | Overlays, dialogs |
| Scale | Expanding cards, zoom details |
| Rotation | Playful interactions |
| Combined | Premium feel, onboarding |