Help us improve
Share bugs, ideas, or general feedback.
Provides best practices for Flutter animations using built-in framework and Material 3 motion tokens. Covers implicit/explicit animations, page transitions, decision tree for approach selection.
npx claudepluginhub verygoodopensource/very_good_claude_code_marketplace --plugin vgv-ai-flutter-pluginHow this skill is triggered — by the user, by Claude, or both
Slash command
/vgv-ai-flutter-plugin:animations [file-or-directory][file-or-directory]This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Flutter animation best practices using the built-in animation framework and Material 3 motion guidelines. No third-party animation libraries (Lottie, Rive, etc.).
Copy-paste animation patterns for React/Next.js: buttons, modals, toasts, stagger, page transitions, exit animations, scroll reveals, and layout transitions, built on motion-foundations tokens and springs.
Provides animation and motion design patterns using Motion (Framer Motion) and View Transitions API for component animations, page transitions, micro-interactions, gestures, exit transitions, and accessibility with prefers-reduced-motion.
Guides SwiftUI animation patterns including implicit/explicit animations, transitions, phase/keyframe animations, Animatable protocol, and @Animatable macro. Use when implementing motion or transitions in views.
Share bugs, ideas, or general feedback.
Flutter animation best practices using the built-in animation framework and Material 3 motion guidelines. No third-party animation libraries (Lottie, Rive, etc.).
Apply these standards to ALL animation work:
AnimationController when an implicit animation sufficesDuration or Curve valuesAppMotion class, not inlineAnimationController must be disposed in the dispose() method of the StateSingleTickerProviderStateMixin for one controller — use TickerProviderStateMixin only when the widget owns multiple controllerswidth/height on complex layouts causes expensive rebuilds; prefer Transform or Opacity which operate on the compositing layerChoose the simplest approach that meets the requirement:
Does the widget rebuild when the value changes?
|
YES --> Does the framework provide an AnimatedFoo widget?
| |
| YES --> Use the implicit AnimatedFoo widget
| | (AnimatedContainer, AnimatedOpacity, AnimatedAlign, etc.)
| |
| NO --> Use TweenAnimationBuilder
|
NO --> Do you need fine-grained control?
(repeat, reverse, sequence, listen to status)
|
YES --> Use AnimationController + AnimatedBuilder
|
NO --> Use TweenAnimationBuilder
Rule of thumb: if the animation is "set a target and let it animate there", use implicit. If the animation must play/pause/reverse/repeat on command, use explicit.
Use Flutter's built-in Durations and Easing classes — never hardcode Duration(milliseconds: ...) or use Curves.* for new code. The framework constants align with the Material 3 motion specification; refer to the Flutter Durations and Easing class documentation for the full token list.
Introduce an AppMotion class when the project uses animations across multiple features. For a single animation in the app, inline M3 tokens are sufficient.
abstract class AppMotion {
// Standard transitions
static const Duration standardDuration = Durations.medium2;
static const Curve standardCurve = Easing.standard;
// Page transitions
static const Duration pageDuration = Durations.medium4;
static const Curve pageEnterCurve = Easing.emphasizedDecelerate;
static const Curve pageExitCurve = Easing.emphasizedAccelerate;
// Fades
static const Duration fadeDuration = Durations.short3;
static const Curve fadeCurve = Easing.standard;
}
Use implicit animations when the widget rebuilds with new target values. The framework interpolates automatically. Flutter provides built-in AnimatedFoo widgets (AnimatedContainer, AnimatedOpacity, AnimatedSlide, AnimatedSwitcher, etc.) — use the one that matches the property being animated. When no built-in widget exists, use TweenAnimationBuilder.
Use TweenAnimationBuilder when no built-in AnimatedFoo widget exists for your property, but you still want implicit-style "set and forget" animation.
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: isActive ? 1.0 : 0.0),
duration: Durations.medium2,
curve: Easing.standard,
builder: (context, value, child) {
return Transform.scale(
scale: 0.8 + (0.2 * value),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: child, // child is not rebuilt — optimization
)
The child parameter is critical: pass widgets that do not depend on the animated value to avoid unnecessary rebuilds.
Use explicit animations when you need control over playback: play, pause, reverse, repeat, or listen to animation status.
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Durations.medium2,
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: Easing.standard,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: child,
);
},
child: child, // static child — not rebuilt each frame
);
}
}
See references/explicit-animations.md for didUpdateWidget patterns, constructor injection for testable controllers, and transition widget vs AnimatedBuilder guidance.
Use Interval inside CurvedAnimation to sequence animations on a single controller:
late final Animation<double> _fadeAnimation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Easing.standard),
);
late final Animation<Offset> _slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.25),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.2, 0.8, curve: Easing.emphasized),
),
);
See references/staggered-animations.md for full staggered entry and staggered list examples. See references/looping-animations.md for repeating and pulse animation patterns.
Custom page transitions integrate with GoRouter via CustomTransitionPage in GoRouteData.buildPage.
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return CustomTransitionPage(
key: state.pageKey,
child: const DetailsPage(),
transitionDuration: Durations.medium4,
reverseTransitionDuration: Durations.medium4,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Easing.emphasizedDecelerate,
),
child: child,
);
},
);
}
See references/page-transitions.md for a reusable AppPageTransitions helper class with fade, slide-fade, and slide-up transitions, and usage with GoRouteData.
Use Hero for shared-element transitions between routes. The framework handles the animation automatically.
// Source screen
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
// Destination screen
Hero(
tag: 'product-image-${product.id}',
child: Image.network(product.imageUrl),
)
Rules for Hero:
Transform and Opacity — these operate on the compositing layer and skip layout/paintchild parameter in AnimatedBuilder and TweenAnimationBuilder to avoid rebuilding static widgets every frameRepaintBoundary around animated widgets in complex layouts to isolate repaintswidth, height, or padding on complex layouts — triggers expensive layout recalculations every frameAnimatedBuilder — only wrap the subtree that changesAnimationController instances for animations that share timing — use Interval on a single controller// Bad — arbitrary values with no semantic meaning
AnimatedContainer(
duration: Duration(milliseconds: 375),
curve: Curves.easeInOutCubic,
// ...
)
// Good — M3 tokens with clear intent
AnimatedContainer(
duration: Durations.medium2,
curve: Easing.standard,
// ...
)
// Bad — memory leak
@override
void dispose() {
super.dispose();
}
// Good — dispose before super.dispose()
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// Bad — entire subtree rebuilds 60 times/second
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value,
child: const ExpensiveWidget(), // rebuilt every frame
);
},
)
// Good — static child passed through
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value,
child: child,
);
},
child: const ExpensiveWidget(), // built once
)
// Bad — unnecessary complexity for a simple target-value animation
class _FadeWidgetState extends State<FadeWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
// ... 20+ lines of boilerplate
// Good — one widget, zero boilerplate
AnimatedOpacity(
duration: Durations.short3,
curve: Easing.standard,
opacity: isVisible ? 1.0 : 0.0,
child: child,
)
| Approach | When to Use |
|---|---|
AnimatedFoo | Built-in widget exists for the property |
TweenAnimationBuilder | Custom property, no playback control needed |
AnimationController | Need play/pause/reverse/repeat/status |
Hero | Shared-element transition between routes |
CustomTransitionPage | Custom GoRouter page transition |
| Mixin | When to Use |
|---|---|
SingleTickerProviderStateMixin | Widget owns exactly one controller |
TickerProviderStateMixin | Widget owns multiple controllers |
didUpdateWidget, testable controllers, transition widgets vs AnimatedBuilderAppPageTransitions helper and GoRouter integration