prefers-reduced-motion Support in Flutter: Respecting User Preferences
Learn how Flutter 3.44+ supports the prefers-reduced-motion setting to automatically disable animations for users who are sensitive to motion.
Published on • September 19, 2026
AI Assistant

prefers-reduced-motion Support in Flutter: Respecting User Preferences
Flutter 3.44 introduced out-of-the-box support for the prefers-reduced-motion browser setting, automatically disabling animations for users who have enabled reduced motion in their operating system. This is a crucial accessibility feature for users with vestibular disorders, seizure disorders, or motion sensitivity.
What is prefers-reduced-motion?
The prefers-reduced-motion CSS media feature detects when a user has requested minimal animations through their operating system settings:
- macOS: System Preferences > Accessibility > Display > Reduce motion
- iOS: Settings > Accessibility > Motion > Reduce Motion
- Android: Settings > Accessibility > Remove animations
- Windows: Settings > Accessibility > Visual effects > Animation effects
Over 30% of users enable some form of reduced motion preference. For these users, animations can cause:
- Nausea and dizziness (vestibular disorders)
- Anxiety and discomfort
- Seizures (in extreme cases)
- Difficulty focusing on content
How Flutter Implements It
Flutter automatically detects the prefers-reduced-motion setting and exposes it through MediaQuery:
final reducedMotion = MediaQuery.of(context).disableAnimations;
if (reducedMotion) {
// Use static alternatives instead of animations
} else {
// Play animations normally
}
Implementing Reduced Motion Support
Basic Approach
class AnimatedWidget extends StatelessWidget {
const AnimatedWidget({super.key});
@override
Widget build(BuildContext context) {
final reduceMotion = MediaQuery.of(context).disableAnimations;
return AnimatedContainer(
duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 300),
curve: Curves.easeInOut,
color: Colors.blue,
child: const Center(
child: Text('Animated Content'),
),
);
}
}
Hero Animations
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override
Widget build(BuildContext context) {
final reduceMotion = MediaQuery.of(context).disableAnimations;
return reduceMotion
? _buildStaticCard()
: Hero(
tag: 'product-${product.id}',
child: _buildStaticCard(),
);
}
Widget _buildStaticCard() {
return Card(
child: Column(
children: [
Image.asset(product.image),
Text(product.name),
Text('\$${product.price}'),
],
),
);
}
}
Custom Animations
class CustomAnimatedWidget extends StatefulWidget {
const CustomAnimatedWidget({super.key});
@override
State<CustomAnimatedWidget> createState() => _CustomAnimatedWidgetState();
}
class _CustomAnimatedWidgetState extends State<CustomAnimatedWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final reduceMotion = MediaQuery.of(context).disableAnimations;
if (reduceMotion) {
_controller = AnimationController(
vsync: this,
value: 1.0, // Skip to end state
);
} else {
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
}
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: child,
);
},
child: const Text('Fade In Content'),
);
}
}
Platform-Specific Behavior
iOS and macOS
When “Reduce Motion” is enabled:
MediaQuery.of(context).disableAnimationsreturnstrue- System animations (page transitions, keyboard appearance) are reduced
- Custom animations should be disabled or simplified
Android
When “Remove animations” is enabled:
MediaQuery.of(context).disableAnimationsreturnstrue- All animations should respect this setting
Web
The prefers-reduced-motion CSS media feature is automatically detected:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Best Practices
1. Always Check the Setting
// Always respect the user's preference
final reduceMotion = MediaQuery.of(context).disableAnimations;
// Use Duration.zero to skip animation
duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 300),
2. Provide Meaningful Alternatives
Instead of completely removing animations, provide static alternatives that convey the same information:
// Instead of a fade-in animation, show the content immediately
// But still provide visual feedback through other means (color, size, etc.)
3. Test Both States
testWidgets('respects reduced motion', (tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(disableAnimations: true),
child: MyApp(),
),
);
// Verify no animations are triggered
});
4. Don’t Override User Preferences
// Bad: Ignoring user preference
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
)..forward();
// Good: Respecting user preference
final reduceMotion = MediaQuery.of(context).disableAnimations;
if (!reduceMotion) {
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
)..forward();
}
Common Patterns
Page Transitions
Route createRoute(Widget page) {
final reduceMotion = MediaQuery.of(navigatorKey.currentContext!).disableAnimations;
if (reduceMotion) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) => child,
);
}
return MaterialPageRoute(builder: (context) => page);
}
Loading Indicators
class LoadingIndicator extends StatelessWidget {
const LoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
final reduceMotion = MediaQuery.of(context).disableAnimations;
if (reduceMotion) {
return const Icon(Icons.hourglass_empty);
}
return const CircularProgressIndicator();
}
}
Resources
- prefers-reduced-motion MDN Documentation
- Flutter 3.44 Release Notes
- Vestibular Disorders Association
Respecting prefers-reduced-motion isn’t just about compliance — it’s about creating an inclusive experience for users with diverse needs. Flutter’s built-in support makes it straightforward to implement this crucial accessibility feature.