image

06 Jan 2026

9K

35K

Implementing a Flutter Shake Animation Effect for Form Error

In modern applications, user experience (UX) plays a pivotal role. When users encounter form validation errors, a static text message can sometimes be overlooked or feel unengaging. A subtle, yet effective, way to draw attention to an invalid input field is through an animation, specifically a "shake" effect. This article explores how to implement a professional shake animation in Flutter to provide clear, immediate feedback for form errors, enhancing the overall user experience.

Why Use a Shake Animation?

  • Instant Feedback: Visually alerts users to an error more effectively than just text.
  • Improved UX: Makes the application feel more dynamic and responsive.
  • Reduced Cognitive Load: Users can quickly identify where the error occurred without having to scan the entire form.
  • Professional Touch: Adds a polished feel to your application.

Core Animation Concepts in Flutter

Flutter's animation system is powerful and flexible. To create a shake effect, we'll leverage:

  1. AnimationController: Manages the animation's state, including starting, stopping, and duration.
  2. Tween: Defines the range of values an animation can interpolate between. For a shake, we'll use a Tween<double> to control horizontal displacement.
  3. AnimatedBuilder: A widget that rebuilds its child whenever the animation changes value, allowing us to apply transformations.
  4. Transform.translate: A widget used to apply translation (movement) to its child.

Step-by-Step Implementation

1. Setup your Widget

Your widget needs to be a StatefulWidget and mix in SingleTickerProviderStateMixin. This mixin provides a Ticker that drives the animation controller.

2. Initialize AnimationController and Animation

Declare an AnimationController and an Animation<double> within your State class. The Animation<double> will be derived from a Tween and the controller.

3. Build the AnimatedBuilder

Wrap the form field you want to shake (e.g., TextFormField) with an AnimatedBuilder. Inside its builder method, use Transform.translate to apply the horizontal offset based on the animation's current value.

4. Triggering the Shake Animation

Create a method that calls _controller.forward() to start the animation and then _controller.reset() to bring it back to the original state. You'll typically call this method when form validation fails.

5. Dispose the Controller

It's crucial to dispose() the AnimationController when the State object is removed to prevent memory leaks.

Example Code

Let's put it all together in a simplified, reusable widget and then integrate it into a form.

First, the reusable ShakeTransition widget and a custom ShakeCurve:


import 'package:flutter/material.dart';
import 'dart:math' as Math;

class ShakeTransition extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final double offset;
  final Function(AnimationController)? onAnimationTrigger;

  const ShakeTransition({
    Key? key,
    required this.child,
    this.duration = const Duration(milliseconds: 400),
    this.offset = 20.0,
    this.onAnimationTrigger,
  }) : super(key: key);

  @override
  _ShakeTransitionState createState() => _ShakeTransitionState();
}

class _ShakeTransitionState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.duration);

    _animation = Tween(begin: 0.0, end: widget.offset).animate(
      CurvedAnimation(
        parent: _controller,
        curve: ShakeCurve(),
      ),
    );

    widget.onAnimationTrigger?.call(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Transform.translate(
          offset: Offset(_animation.value, 0),
          child: widget.child,
        );
      },
      child: widget.child,
    );
  }
}

// Custom Shake Curve
class ShakeCurve extends Curve {
  @override
  double transformInternal(double t) {
    // Math.sin(x) for oscillation, 4 * Math.PI for 2 full shakes
    return (1 - t) * Math.sin(t * 4 * Math.PI);
  }
}

Next, how to integrate ShakeTransition with a TextFormField and trigger the animation on validation error:


import 'package:flutter/material.dart';
import 'dart:math' as Math; // Required for ShakeCurve if not in separate file

// Assuming ShakeTransition and ShakeCurve definitions are available (e.g., from above code block)

class LoginForm extends StatefulWidget {
  const LoginForm({Key? key}) : super(key: key);

  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State {
  final _formKey = GlobalKey();
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  // Animation controllers for shake effect
  late AnimationController _usernameShakeController;
  late AnimationController _passwordShakeController;

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      // If form is valid, proceed with login
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Processing Data')),
      );
      // Simulate network request
      Future.delayed(const Duration(seconds: 2), () {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Login Successful!')),
        );
      });
    } else {
      // If form is invalid, shake the invalid fields
      if (_usernameController.text.isEmpty) {
        _usernameShakeController.forward().then((_) => _usernameShakeController.reset());
      }
      if (_passwordController.text.isEmpty) {
        _passwordShakeController.forward().then((_) => _passwordShakeController.reset());
      }
    }
  }

  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    // Dispose the shake controllers
    // These are initialized via `onAnimationTrigger` callback, so they need to be disposed here.
    _usernameShakeController.dispose();
    _passwordShakeController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login Form')),
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16.0),
          child: Form(
            key: _formKey,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ShakeTransition(
                  onAnimationTrigger: (controller) {
                    _usernameShakeController = controller;
                  },
                  child: TextFormField(
                    controller: _usernameController,
                    decoration: const InputDecoration(
                      labelText: 'Username',
                      border: OutlineInputBorder(),
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your username';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(height: 20),
                ShakeTransition(
                  onAnimationTrigger: (controller) {
                    _passwordShakeController = controller;
                  },
                  child: TextFormField(
                    controller: _passwordController,
                    obscureText: true,
                    decoration: const InputDecoration(
                      labelText: 'Password',
                      border: OutlineInputBorder(),
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your password';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(height: 30),
                ElevatedButton(
                  onPressed: _submitForm,
                  child: const Text('Login'),
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
                    textStyle: const TextStyle(fontSize: 18),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Explanation of Key Code Segments

  • ShakeTransition's State class mixes in SingleTickerProviderStateMixin to provide a `Ticker` for the AnimationController.
  • The AnimationController is initialized with a duration and vsync: this.
  • _animation is created using a Tween<double> (from 0.0 to widget.offset) and a CurvedAnimation with a ShakeCurve.
  • ShakeCurve: This is a custom Curve that uses Math.sin to create an oscillating movement. The (1 - t) factor makes the shake amplitude decrease over time, and t * 4 * Math.PI ensures two full oscillations for a noticeable shake.
  • AnimatedBuilder: This widget rebuilds its child (the TextFormField in our case) whenever the _animation value changes, applying a Transform.translate with the current _animation.value as the horizontal offset.
  • onAnimationTrigger: This callback in ShakeTransition is used to expose the internal AnimationController to its parent widget (_LoginFormState). This allows the parent to control when the shake animation should play.
  • In _LoginFormState, we store the received AnimationControllers. When a form validation fails (e.g., a field is empty), we call _usernameShakeController.forward().then((_) => _usernameShakeController.reset()); to play the animation and then reset it to its initial state.
  • Proper dispose() calls are essential for both TextEditingControllers and AnimationControllers to prevent memory leaks.

Refinements and Best Practices

  • Duration and Offset: Experiment with the duration and offset values in ShakeTransition to find the perfect balance that is noticeable but not jarring for your users.
  • Curves: While ShakeCurve provides a specific effect, you can experiment with other built-in curves or create custom ones for different shake characteristics (e.g., more subtle, faster decay).
  • Reusability: The ShakeTransition widget is designed for reusability, allowing you to easily apply the shake effect to different form fields or other widgets throughout your application.
  • Accessibility: Always ensure that the shake animation is accompanied by clear, descriptive error messages (as provided by TextFormField's validator) for users who might not perceive the animation or rely on screen readers.
  • Performance: For complex forms with many fields, consider triggering the animation only for the first invalid field or fields currently in view to optimize performance. However, for most forms, this approach is perfectly efficient.

Conclusion

Implementing a shake animation for form errors in Flutter is a straightforward yet powerful way to elevate your application's user experience. By leveraging Flutter's robust animation framework with AnimationController, Tween, and AnimatedBuilder, you can provide immediate and intuitive visual feedback, making your forms more engaging and user-friendly. This professional touch not only improves usability but also contributes to a more polished and responsive application design.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is