image

26 Feb 2026

9K

35K

Flutter Animation: Slide & Scale on Card Tap Interaction

Micro-interactions play a crucial role in enhancing user experience by providing visual feedback and making an application feel more alive and responsive. In Flutter, creating such delightful animations is straightforward thanks to its powerful animation framework. This article will guide you through implementing a common and engaging animation: a slide and scale effect on a card when it is tapped.

Understanding the Core Animation Concepts in Flutter

Before diving into the code, let's briefly touch upon the core components of Flutter's animation system we'll be using:

  • AnimationController: Manages the animation. It can be started, stopped, reversed, and gives notification about its progress.
  • Tween: Defines the range of values an animation can interpolate between. For example, a Tween for scaling from 1.0 to 1.1, or a Tween for sliding.
  • CurvedAnimation: Applies a non-linear curve to an animation controller's progress, making animations feel more natural (e.g., ease-in, ease-out).
  • AnimatedBuilder: A widget that rebuilds its children whenever the animation changes value. This is highly efficient as it only rebuilds the animated part of the widget tree.
  • Transform Widgets: Widgets like Transform.scale and Transform.translate are used to apply scale, position, and rotation transformations to their child widgets.

Setting Up Your Project

First, create a new Flutter project if you haven't already:


flutter create card_animation_demo
cd card_animation_demo

Now, open lib/main.dart. We'll start with a basic structure:


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Card Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Interactive Card'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: AnimatedCard(), // Our custom card widget will go here
        ),
      ),
    );
  }
}

Designing the Interactive Card Widget

Our interactive card needs to manage its animation state, so it will be a StatefulWidget. Let's create a new file, say lib/animated_card.dart, or directly implement it in main.dart for simplicity.

For this example, we'll implement it directly within main.dart.


// ... (previous code)

class AnimatedCard extends StatefulWidget {
  const AnimatedCard({super.key});

  @override
  State createState() => _AnimatedCardState();
}

class _AnimatedCardState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _slideAnimation;
  late Animation _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    _slideAnimation = Tween(
      begin: Offset.zero,
      end: const Offset(0, -0.05), // Slide up by 5% of its height
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutBack,
    ));

    _scaleAnimation = Tween(
      begin: 1.0,
      end: 1.05, // Scale up by 5%
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutBack,
    ));
  }

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

  void _handleTap() {
    if (_controller.isDismissed) {
      _controller.forward();
    } else if (_controller.isCompleted) {
      _controller.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Transform.translate(
            offset: _slideAnimation.value * 20, // Multiply by a factor for more visible slide
            child: Transform.scale(
              scale: _scaleAnimation.value,
              child: Card(
                elevation: 4,
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
                child: const SizedBox(
                  width: 300,
                  height: 200,
                  child: Center(
                    child: Text(
                      'Tap Me!',
                      style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

Full Code Example

Here's the complete main.dart file for a runnable example:


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Card Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Interactive Card'),
        centerTitle: true,
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: AnimatedCard(),
        ),
      ),
    );
  }
}

class AnimatedCard extends StatefulWidget {
  const AnimatedCard({super.key});

  @override
  State createState() => _AnimatedCardState();
}

class _AnimatedCardState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _slideAnimation;
  late Animation _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    // Slide animation: Moves the card up
    _slideAnimation = Tween(
      begin: Offset.zero,
      end: const Offset(0, -0.1), // Slide up by 10% of its own height
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutBack, // A bouncy effect
    ));

    // Scale animation: Enlarges the card slightly
    _scaleAnimation = Tween(
      begin: 1.0,
      end: 1.05, // Scales up by 5%
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutBack,
    ));
  }

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

  void _handleTap() {
    if (_controller.isDismissed) {
      _controller.forward(); // Start animation
    } else if (_controller.isCompleted) {
      _controller.reverse(); // Reverse animation
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Transform.translate(
            offset: _slideAnimation.value * 20, // Apply the slide offset. Multiplying by a factor makes it more visible.
            child: Transform.scale(
              scale: _scaleAnimation.value, // Apply the scale value
              alignment: Alignment.center, // Scale from the center
              child: Card(
                elevation: 4,
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
                child: const SizedBox(
                  width: 300,
                  height: 200,
                  child: Center(
                    child: Text(
                      'Tap Me!',
                      style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blueAccent),
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

Code Explanation

Let's break down the key parts of the _AnimatedCardState:

  • with SingleTickerProviderStateMixin: This mixin provides the vsync object required by AnimationController. It ensures that animations only consume resources when they are visible.
  • _controller = AnimationController(...):
    • vsync: this: Links the controller to the widget's lifecycle.
    • duration: const Duration(milliseconds: 300): Sets the total time for the animation to complete.
  • _slideAnimation = Tween(...).animate(...):
    • A Tween is used for the slide effect, defining the start (Offset.zero, no change) and end (Offset(0, -0.1), moving up 10% of the widget's height).
    • CurvedAnimation(parent: _controller, curve: Curves.easeOutBack): Wraps the controller to apply an easeOutBack curve, giving a slight "overshoot" effect before settling, which makes the animation more dynamic.
  • _scaleAnimation = Tween(...).animate(...):
    • A Tween is used for the scale effect, interpolating from 1.0 (original size) to 1.05 (5% larger).
    • It also uses the same CurvedAnimation for consistent timing and feel.
  • _handleTap():
    • This method is called when the GestureDetector detects a tap.
    • _controller.isDismissed checks if the animation is at its start state. If so, _controller.forward() starts it.
    • _controller.isCompleted checks if the animation is at its end state. If so, _controller.reverse() plays it backward. This creates the toggle effect.
  • AnimatedBuilder(animation: _controller, builder: ...):
    • This widget is crucial. It listens to _controller and rebuilds its builder callback whenever the animation's value changes.
    • Inside the builder, Transform.translate and Transform.scale are used.
      • Transform.translate applies the _slideAnimation.value to move the card. We multiply _slideAnimation.value by 20 to make the slide more noticeable, as Offset(0, -0.1) provides a normalized value relative to the card's size.
      • Transform.scale applies the _scaleAnimation.value to change the size of the card. alignment: Alignment.center ensures the scaling happens from the center of the card.
  • dispose(): It's critical to dispose of the AnimationController when the state object is removed from the tree to prevent memory leaks.

Conclusion

You've successfully implemented a engaging slide and scale animation for a card in Flutter. This pattern of using AnimationController, Tween, CurvedAnimation, and AnimatedBuilder is fundamental to creating most explicit animations in Flutter. By understanding these core concepts, you can create a wide array of sophisticated and user-friendly animations, making your application stand out. Feel free to experiment with different Tween values, Curves, and durations to achieve unique visual effects!

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