NibMotion
NibMotion is a declarative, physics-based animation layer for Flutter. If
you’ve used Framer Motion in React, the mental model will feel familiar:
instead of wiring up AnimationControllers, Tweens, and AnimatedBuilders
by hand, you describe what a widget should look like in different states —
initial, animate, whileTap, whileHover, and more — and NibMotion
figures out how to animate between those states for you.
Under the hood, every animatable property (opacity, position, scale,
rotation, skew, color, border radius, and box shadow) is driven by its own
MotionValue, interpolated either with a curve-based tween or a real spring
simulation. Because these values are pushed straight to the render layer,
animating a widget with NibMotion does not trigger Flutter widget rebuilds.
NibMotion ships as a single, self-contained package with zero required pub dependencies beyond the Flutter SDK itself — it adds no transitive dependencies to your app.
Note: This page covers installation, the core
NibMotionwidget, global configuration viaNibMotionConfig, the entrance gate (NibMotionGate), and reduced-motion support. Gestures, drag, viewport-triggered animations, variants, keyframes, and motion values each have their own dedicated page — linked from the API reference tables below and the sidebar.
Installation
Add the package to your pubspec.yaml with the Flutter CLI:
flutter pub add nib_motionThen import it wherever you want to use NibMotion or any of its supporting
classes:
import 'package:nib_motion/nib_motion.dart';That’s it — there’s no code generation step, no build runner, and no
additional native setup. NibMotion is a plain Flutter widget you can drop
into any existing widget tree.
The NibMotion widget
The core building block is the NibMotion widget. It wraps a single child
and accepts a set of NibAnim values describing what that child should look
like in different states. The two you’ll use on almost every widget are:
initial— the appearance on the very first frame, before any animation runs.animate— the appearance NibMotion animates toward once the widget has mounted (or wheneveranimatechanges on a rebuild).
Here’s a minimal fade-in: the child starts fully transparent (opacity: 0)
and animates to fully visible (opacity: 1) as soon as it appears on screen.
import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
class WelcomeCard extends StatelessWidget {
const WelcomeCard({super.key});
@override
Widget build(BuildContext context) {
return NibMotion(
initial: const NibAnim(opacity: 0),
animate: const NibAnim(opacity: 1),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: const Text('Welcome to NibMotion!'),
),
);
}
}When this widget first builds, NibMotion:
- Applies
initialimmediately — theContaineris invisible. - Schedules a post-frame callback, waits for
NibMotionGateand the configured warm-up window (see below), and then animates every property present inanimatefrom itsinitialvalue toward itsanimatevalue using the activeNibTransition(covered in full on the Transitions page).
Any property you omit from NibAnim is left untouched — a NibAnim(opacity: 0) only animates opacity, regardless of what other properties a sibling
NibMotion might be animating. This is why NibAnim treats every field as
nullable: null means “don’t touch this,” not “set this to zero.”
You can combine multiple properties in a single NibAnim. For example, a
“slide up and fade in” entrance:
NibMotion(
initial: const NibAnim(opacity: 0, y: 24),
animate: const NibAnim(opacity: 1, y: 0),
child: const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Slides up and fades in'),
),
),
)NibMotion also exposes whileTap, whileHover, whileFocus, whileDrag,
and whileInView for interaction- and visibility-driven animations, plus
variants, keyframes, motionValues, and controller for more advanced
choreography. Each of these is summarized in the API reference below and
covered in depth on its own page.
NibMotionConfig
NibMotionConfig is an InheritedWidget you place near the root of your app
— typically wrapping MaterialApp (or CupertinoApp/WidgetsApp) — to set
defaults that every descendant NibMotion picks up automatically.
import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
void main() {
runApp(
NibMotionConfig(
defaultTransition: const NibTransition(
duration: Duration(milliseconds: 250),
curve: Curves.easeOut,
),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: const HomeScreen(),
);
}
}The three configuration knobs are:
reducedMotion— when set totrueorfalse, this overrides the platform’sMediaQuery.disableAnimationssetting for every descendantNibMotion. Leave itnull(the default) to defer to the platform accessibility setting. See Reduced motion below for what this actually changes.defaultTransition— theNibTransitionused by anyNibMotionthat doesn’t specify its owntransition. Without aNibMotionConfigancestor (or when this field is left at its default),NibMotionfalls back toconst NibTransition()’s own defaults. Full details onNibTransition, including spring vs. curve-based tweens, are on the Transitions page.entranceWarmup— how long after the app startsNibMotionentrance animations (animate/keyframeson first build, and an immediately-visiblewhileInView) should wait before starting. Defaults toDuration(milliseconds: 400).
Why entranceWarmup exists
On a real device there’s a brief gap between Flutter rendering its first frame
and the platform’s native splash screen actually disappearing. Without any
warm-up, an entrance animation could start — and even finish — while it’s
still hidden behind that splash screen, so the user never sees it play.
entranceWarmup absorbs that gap: any NibMotion created within the warm-up
window waits until the window elapses before starting its entrance animation.
Widgets created after the warm-up window has already elapsed (for example,
on a screen the user navigates to later) start immediately, with no delay.
The default of 400ms comfortably covers this gap for most apps. If your app
preserves its native splash screen for much longer than that (for example, via
flutter_native_splash in “preserve” mode), use NibMotionGate instead — see
the next section.
NibMotionGate
NibMotionGate is a global gate — separate from NibMotionConfig — that
holds every NibMotion’s entrance animation at its initial frame until your
app explicitly signals that it’s ready to be seen.
By default the gate is open: NibMotionGate.ready starts as true, so
entrance animations only wait for the entranceWarmup window described above.
Apps that preserve a native splash screen for longer than the default warm-up
— and need to guarantee entrance animations don’t run underneath it — can
hold the gate before runApp and release it once the splash screen is
actually gone:
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:nib_motion/nib_motion.dart';
Future<void> main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
// Hold every NibMotion entrance animation until we explicitly release it.
NibMotionGate.hold();
runApp(const MyApp());
// Run your async startup work (loading config, checking auth, etc.)...
await initializeApp();
// Now it's safe to remove the splash screen and let entrance
// animations play.
FlutterNativeSplash.remove();
NibMotionGate.release();
}While the gate is held (NibMotionGate.ready.value == false), every
NibMotion with an animate, keyframes, repeatAnimationInLoop, or an
immediately-visible whileInView simply sits at its initial (or first
keyframe) appearance — no animation runs, and no time is “wasted” animating
something the user can’t see yet. As soon as release() is called, all of
those held animations begin, each still respecting its own
entranceWarmup if that window hasn’t elapsed yet.
NibMotionGate exposes four static members:
ready— aValueNotifier<bool>thatNibMotionwidgets listen to.truemeans entrance animations are allowed to proceed (subject toentranceWarmup);falsemeans they’re held at their initial frame.hold()— setsreadytofalse, holding all entrance animations.release()— setsreadytotrue, releasing any held animations.reset()— resetsreadyback to its default open (true) state. Intended for use in tests, so each test starts from a known gate state.
Note: Most apps never need to call
NibMotionGatedirectly — the default open gate plus the 400msentranceWarmupis enough. Reach forNibMotionGate.hold()/release()specifically when you’re preserving a native splash screen for an async startup sequence longer than 400ms.
Reduced motion
Some users enable a system-level “reduce motion” accessibility setting.
Flutter surfaces this as MediaQuery.of(context).disableAnimations, and
NibMotion respects it automatically — every NibMotion checks
NibMotionConfig.shouldReduceMotion(context) before starting an animation.
Resolution order is:
- A test-only global override (
NibMotionConfig.setReducedMotion), if set. NibMotionConfig.reducedMotion, if aNibMotionConfigancestor sets it totrueorfalse.- Otherwise,
MediaQuery.of(context).disableAnimations— the platform accessibility setting.
When reduced motion is in effect, NibMotion does not simply skip
animations and leave the widget at initial — it jumps straight to the
final state with no visible transition:
- An
animate/keyframesentrance jumps immediately to theanimatetarget (or the last keyframe). whileTap,whileHover,whileFocus,whileDrag, andwhileInViewtransitions still apply their target state, just instantly instead of animating.repeatAnimationInLoopjumps to the end of one cycle and does not loop forever — looping animations would be especially disruptive for users who asked for reduced motion.
This means your UI ends up in exactly the same final layout and visual state
either way — reduced motion only removes the motion, not the result. To
force this behavior on for testing (regardless of the platform setting or any
NibMotionConfig ancestor), call:
NibMotionConfig.setReducedMotion(true);
// ... run your test ...
NibMotionConfig.setReducedMotion(null); // clear the overrideAPI reference
NibMotion
| Prop | Type | Default | Description |
|---|---|---|---|
key | Key? | null | Required (non-null) if exit is set, so an ancestor NibPresence can track this widget across removals. |
initial | NibAnim | String? | null (treated as NibAnim.identity) | The appearance applied immediately on first build, before any transition runs. Either a NibAnim or a String naming an entry in variants. |
animate | NibAnim | String? | null | The appearance to animate toward. null means “no change” unless variants is set and an ancestor NibMotion propagates a matching variant name. Either a NibAnim or a String naming an entry in variants; when a String, the name is also propagated to descendants with a matching variants entry and no explicit animate. |
exit | NibAnim? | null | The appearance to animate toward when an ancestor NibPresence removes this widget. Requires key to be set. |
whileTap | NibAnim? | null | The appearance to animate toward while this widget is pressed. Part of the gestures system — see Gestures. |
whileHover | NibAnim? | null | The appearance to animate toward while the pointer hovers over this widget. See Gestures. |
whileFocus | NibAnim? | null | The appearance to animate toward while this widget has input focus. See Gestures. |
whileDrag | NibAnim? | null | The appearance to animate toward while this widget is being dragged; reverts to animate (or initial) when the drag ends. See Drag. |
drag | NibDragConfig? | null | Enables dragging this widget when set. Full configuration (axis constraints, snap points, release behavior) is covered on the Drag page. |
whileInView | NibAnim? | null | The appearance to animate toward while this widget is visible on screen, according to viewport. Full details on the Viewport page. |
viewport | NibInViewConfig? | NibInViewConfig() defaults (when whileInView is set) | Configures the visibility check used by whileInView (margins, visible-area threshold, once). See Viewport. |
variants | NibVariants? | null | Named animation states this widget resolves initial/animate string names against, and matches against variant names propagated from an ancestor NibMotion. Full details on the Variants page. |
keyframes | List<NibKeyframe>? | null | A multi-step enter animation that overrides initial/animate, driven by a single tween from 0.0 to 1.0. Not compatible with spring transitions. See Keyframes. |
motionValues | NibMotionValues? | null | Pre-built MotionValues that drive this widget’s animatable properties directly, bypassing the internal spring/tween system for any property they provide. See Motion values. |
transition | NibTransition? | null (falls back to NibMotionConfig.defaultTransitionOf(context), or NibTransition() defaults) | The transition used for animate and as the default for gesture states that don’t specify their own. Full details on the Transitions page. |
controller | NibMotionController? | null | An optional external handle for imperatively driving this widget’s animations (e.g. replaying the entrance, playing keyframes on demand). See Controllers. |
repeatAnimationInLoop | bool | false | When true, instead of a one-shot entrance animation, this widget ping-pongs forever between initial and animate (or between keyframe 0.0 and 1.0), using the active transition’s duration/curve. Has no effect if motionValues is set. Under reduced motion, jumps to the end of one cycle and does not loop. |
child | Widget | (required) | The widget below this one in the tree. |
NibMotionConfig
| Prop | Type | Default | Description |
|---|---|---|---|
reducedMotion | bool? | null | Overrides MediaQuery.of(context).disableAnimations when non-null. null defers to the platform accessibility setting. |
defaultTransition | NibTransition | const NibTransition() | The transition used by descendant NibMotion widgets that don’t specify their own transition. See Transitions. |
entranceWarmup | Duration | Duration(milliseconds: 400) | How long after the app starts entrance animations should wait before starting, absorbing the gap between Flutter’s first frame and the native splash screen disappearing. |
child | Widget | (required) | The subtree this configuration applies to — typically your whole app. |
NibMotionGate
| Prop | Type | Default | Description |
|---|---|---|---|
ready | ValueNotifier<bool> (static) | starts true | Whether entrance animations are allowed to start. NibMotion widgets listen to this and hold their entrance animation at its initial frame while it’s false. |
hold() | static void | — | Sets ready to false, holding all NibMotion entrance animations at their initial frame until release() is called. |
release() | static void | — | Sets ready to true, releasing any entrance animations held by hold(). |
reset() | static void | — | Resets the gate back to its default open (true) state. Intended for tests. |
Next steps
- Animate — a deeper look at
initial/animate,NibAnim’s full set of animatable properties, and how values interpolate between states. - Transitions — configure
NibTransitionto control how animations move: curve-based tweens vs. spring physics, duration, delay, and stagger.