Skip to content
Blog

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).disableAnimations returns true
  • System animations (page transitions, keyboard appearance) are reduced
  • Custom animations should be disabled or simplified

Android

When “Remove animations” is enabled:

  • MediaQuery.of(context).disableAnimations returns true
  • 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

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.