How to implement a shake text effect in Flutter

Source code on GitHub

I really love how Flutter animations can be used to improve usability.

Here's an example showing a Text widget that shakes when some error occurs.

The Flutter animation APIs make it very easy to implement this. Here's a step-by-step guide. 👇

1. Create a custom sine curve

The effect is accomplished using an AnimationController and a custom curve based on the sine function.

First of all, here's a SineCurve that repeats the sine function count times over a 2 * pi period.

// 1. custom Curve subclass class SineCurve extends Curve { SineCurve({this.count = 3}); final double count; // 2. override transformInternal() method @override double transformInternal(double t) { return sin(count * 2 * pi * t); } }

Since SineCurve is a Curve subclass, it can be given as an argument to any implicitly animated widget.

2. Abstract the AnimationController boilerplate code

We'll need an AnimationController to get the effect we want. To reduce the boilerplate code, let's define a State subclass:

abstract class AnimationControllerState<T extends StatefulWidget> extends State<T> with SingleTickerProviderStateMixin { AnimationControllerState(this.animationDuration); final Duration animationDuration; late final animationController = AnimationController( vsync: this, duration: animationDuration ); @override void dispose() { animationController.dispose(); super.dispose(); } }

For more details about this technique, see: How to reduce AnimationController boilerplate code: Flutter Hooks vs extending the State class

3. Create a custom ShakeWidget

Let's define a StatefulWidget subclass that takes a child widget along with some customizable properties:

class ShakeWidget extends StatefulWidget { const ShakeWidget({ Key? key, required this.child, required this.shakeOffset, this.shakeCount = 3, this.shakeDuration = const Duration(milliseconds: 500), }) : super(key: key); // 1. pass a child widget final Widget child; // 2. configurable properties final double shakeOffset; final int shakeCount; final Duration shakeDuration; // 3. pass the shakeDuration as an argument to ShakeWidgetState. See below. @override ShakeWidgetState createState() => ShakeWidgetState(shakeDuration); }

4. Create a custom CurvedAnimation

Let's define the ShakeWidgetState class with a custom animation:

// note: ShakeWidgetState is public class ShakeWidgetState extends AnimationControllerState<ShakeWidget> { ShakeWidgetState(Duration duration) : super(duration); // 1. create a Tween late Animation<double> _sineAnimation = Tween( begin: 0.0, end: 1.0, // 2. animate it with a CurvedAnimation ).animate(CurvedAnimation( parent: animationController, // 3. use our SineCurve curve: SineCurve(count: widget.shakeCount.toDouble()), )); }

5. Use the animation with AnimatedBuilder and Transform.translate

Let's define a build() method with a custom AnimatedBuilder:

@override Widget build(BuildContext context) { // 1. return an AnimatedBuilder return AnimatedBuilder( // 2. pass our custom animation as an argument animation: _sineAnimation, // 3. optimization: pass the given child as an argument child: widget.child, builder: (context, child) { return Transform.translate( // 4. apply a translation as a function of the animation value offset: Offset(_sineAnimation.value * widget.shakeOffset, 0), // 5. use the child widget child: child, ); }, ); }

Note how we're passing _sineAnimation as an argument to the AnimatedBuilder, and also use it to calculate the offset value. See below for an alternative approach.

6. Add a status listener to reset the animation when complete

As we want to "play" the animation many times, we need to reset the AnimationController when the animation is complete:

@override void initState() { super.initState(); // 1. register a status listener animationController.addStatusListener(_updateStatus); } @override void dispose() { // 2. dispose it when done animationController.removeStatusListener(_updateStatus); super.dispose(); } void _updateStatus(AnimationStatus status) { // 3. reset animationController when the animation is complete if (status == AnimationStatus.completed) { animationController.reset(); } }

7. Add a shake() method

Our ShakeWidgetState class needs a shake() method that we can call from outside to start the animation:

// note: this method is public void shake() { animationController.forward(); }

8. Control the ShakeWidget with a GlobalKey

In the parent widget we can declare a GlobalKey<ShakeWidgetState>, and use it to call shake() when a button is pressed.

class MyHomePage extends StatelessWidget { // 1. declare a GlobalKey final _shakeKey = GlobalKey<ShakeWidgetState>(); @override Widget build(BuildContext context) { return Column( children: [ // 2. shake the widget via the GlobalKey when a button is pressed ElevatedButton( child: Text('Sign In', style: TextStyle(fontSize: 20)), onPressed: () => _shakeKey.currentState?.shake(), ), // 3. Add a parent ShakeWidget to the child widget we want to animate ShakeWidget( // 4. pass the GlobalKey as an argument key: _shakeKey, // 5. configure the animation parameters shakeCount: 3, shakeOffset: 10, shakeDuration: Duration(milliseconds: 400), // 6. Add the child widget that will be animated child: Text( 'Invalid credentials', textAlign: TextAlign.center, style: TextStyle( color: Colors.red, fontSize: 20, fontWeight: FontWeight.bold), ), ), ], ); } }

Here's the final result (with a slightly more interesting UI):

Final Notes: implicit vs explicit animations

The SineCurve class we created is a Curve subclass, so it can be given as an argument to any implicitly animated widget.

In this example, we used it to create a custom CurvedAnimation that is passed as an argument to our AnimatedBuilder.

But since we're using an explicit animation, we don't need the SineCurve or even the _sineAnimation to start with. In fact, we can get the same result by calculating the sine value directly inside the AnimatedBuilder code:

@override Widget build(BuildContext context) { // 1. return an AnimatedBuilder return AnimatedBuilder( // 2. pass the AnimationController as an argument animation: animationController, // 3. optimization: pass the given child as an argument child: widget.child, builder: (context, child) { // 4. calculate the sine value directly final sineValue = sin(widget.shakeCount * 2 * pi * animationController.value); return Transform.translate( // 5. apply a translation as a function of the animation value offset: Offset(sineValue * widget.shakeOffset, 0), // 6. use the child widget child: child, ); }, ); }

Here's the full source code for this example:

That's it. Now go ahead and shake your widgets! 😎

Happy coding!

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter In Production

Flutter In Production

Learn about flavors, environments, error monitoring, analytics, release management, CI/CD, and finally ship your Flutter apps to the stores. 🚀

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.