Every animation,
interactive.
Explore NibMotion's full animation API with live Flutter demos and copyable code.
Fade
Opacity-based enter and exit. The simplest and most versatile animation.
NibMotion(
initial: NibAnim(opacity: 0),
animate: NibAnim(opacity: 1),
exit: NibAnim(opacity: 0),
transition: const NibTransition(),
child: MyWidget(),
)Fade In
Opacity-only entrance. Simplest NibMotion animation — no transform involved.
class FadeInExample extends StatefulWidget {
const FadeInExample({super.key});
@override
State<FadeInExample> createState() => _FadeInExampleState();
}
class _FadeInExampleState extends State<FadeInExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(opacity: 0),
animate: const NibAnim(opacity: 1),
transition: const NibTransition(duration: Duration(milliseconds: 600)),
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Fade In',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Slide Up
Pure Y-axis entrance with no opacity change. Card rises into place.
class SlideUpExample extends StatefulWidget {
const SlideUpExample({super.key});
@override
State<SlideUpExample> createState() => _SlideUpExampleState();
}
class _SlideUpExampleState extends State<SlideUpExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(y: 60, opacity: 0),
animate: const NibAnim(y: 0, opacity: 1),
transition: const NibTransition(
spring: NibSpringDescription.gentle,
duration: Duration(milliseconds: 650),
),
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Slide Up',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Slide Horizontal
Slide in from the right using the X axis. Useful for drawer panels and side sheets.
class SlideHorizontalExample extends StatefulWidget {
const SlideHorizontalExample({super.key});
@override
State<SlideHorizontalExample> createState() => _SlideHorizontalExampleState();
}
class _SlideHorizontalExampleState extends State<SlideHorizontalExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(x: 110, opacity: 0),
animate: const NibAnim(x: 0, opacity: 1),
transition: const NibTransition(
spring: NibSpringDescription.snappy,
duration: Duration(milliseconds: 600),
),
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Slide In ←',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Fade + Slide
Combined opacity and Y offset — the most common entrance pattern.
class FadeWithSlideExample extends StatefulWidget {
const FadeWithSlideExample({super.key});
@override
State<FadeWithSlideExample> createState() => _FadeWithSlideExampleState();
}
class _FadeWithSlideExampleState extends State<FadeWithSlideExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(opacity: 0, y: 24),
animate: const NibAnim(opacity: 1, y: 0),
transition: NibTransition.springGentle,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Fade + Slide',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Exit
Intercepts widget removal, plays the exit animation to completion, then unmounts.
NibPresence(
children: isVisible
? [
NibMotion(
key: const ValueKey('modal'),
initial: NibAnim(opacity: 0, scale: 0.94),
animate: NibAnim(opacity: 1, scale: 1.0),
exit: NibAnim(opacity: 0, scale: 0.94),
transition: NibTransition.springSnappy,
child: MyModal(),
),
]
: [],
)Presence Toggle
NibPresence intercepts widget removal so the exit animation plays to completion before unmount.
class PresenceToggleExample extends StatefulWidget {
const PresenceToggleExample({super.key});
@override
State<PresenceToggleExample> createState() => _PresenceToggleExampleState();
}
class _PresenceToggleExampleState extends State<PresenceToggleExample> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 140,
child: Center(
child: NibPresence(
children: _visible
? [
NibMotion(
key: const ValueKey('card'),
initial: const NibAnim(opacity: 0, scale: 0.9),
animate: const NibAnim(opacity: 1, scale: 1.0),
exit: const NibAnim(opacity: 0, scale: 0.9),
transition: NibTransition.springSnappy,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Toggle Me',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
]
: [],
),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => setState(() => _visible = !_visible),
style: FilledButton.styleFrom(backgroundColor: const Color(0xFF1C2537)),
child: Text(
_visible ? 'Remove' : 'Show',
style: const TextStyle(color: Color(0xFF54C5F8)),
),
),
],
),
),
);
}
}Slide Exit
Widget slides off-screen on removal. Useful for toasts, drawers, and bottom sheets.
class SlideExitExample extends StatefulWidget {
const SlideExitExample({super.key});
@override
State<SlideExitExample> createState() => _SlideExitExampleState();
}
class _SlideExitExampleState extends State<SlideExitExample> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 140,
child: Center(
child: NibPresence(
children: _visible
? [
NibMotion(
key: const ValueKey('toast'),
initial: const NibAnim(x: 300, opacity: 0),
animate: const NibAnim(x: 0, opacity: 1),
exit: const NibAnim(x: 300, opacity: 0),
transition: NibTransition.springSnappy,
child: Container(
width: 200,
height: 60,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'→ Slide Exit',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
),
]
: [],
),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => setState(() => _visible = !_visible),
style: FilledButton.styleFrom(backgroundColor: const Color(0xFF1C2537)),
child: Text(
_visible ? 'Dismiss' : 'Show',
style: const TextStyle(color: Color(0xFF54C5F8)),
),
),
],
),
),
);
}
}Scale + Fade Exit
Scales down and fades out on removal — natural for modals and dialogs.
class ScaleExitExample extends StatefulWidget {
const ScaleExitExample({super.key});
@override
State<ScaleExitExample> createState() => _ScaleExitExampleState();
}
class _ScaleExitExampleState extends State<ScaleExitExample> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 160,
child: Center(
child: NibPresence(
children: _visible
? [
NibMotion(
key: const ValueKey('modal'),
initial: const NibAnim(opacity: 0, scale: 0.85),
animate: const NibAnim(opacity: 1, scale: 1.0),
exit: const NibAnim(opacity: 0, scale: 0.85),
transition: NibTransition.springGentle,
child: Container(
width: 200,
height: 120,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Scale Exit',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
]
: [],
),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => setState(() => _visible = !_visible),
style: FilledButton.styleFrom(backgroundColor: const Color(0xFF1C2537)),
child: Text(
_visible ? 'Dismiss' : 'Show',
style: const TextStyle(color: Color(0xFF54C5F8)),
),
),
],
),
),
);
}
}Gesture
Declare gesture states — NibMotion handles the rest. whileTap, whileHover, whileDrag.
NibMotion(
whileTap: NibAnim(scale: 0.92, opacity: 0.85),
whileHover: NibAnim(scale: 1.06, y: -4),
transition: NibTransition.springSnappy,
child: MyButton(),
)While Tap
Animates to target values while pressed, springs back on release.
class WhileTapExample extends StatelessWidget {
const WhileTapExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: NibMotion(
whileTap: const NibAnim(scale: 0.4),
transition: NibTransition.springSnappy,
child: GestureDetector(
onTap: () {},
child: Container(
width: 180,
height: 56,
decoration: BoxDecoration(
color: const Color(0xFF54C5F8),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'Tap Me',
style: TextStyle(
color: Color(0xFF07080D),
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
),
),
),
),
);
}
}While Hover
Responds to pointer hover — useful for desktop and web Flutter targets.
class WhileHoverExample extends StatelessWidget {
const WhileHoverExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: NibMotion(
whileHover: const NibAnim(scale: 1.05, y: -4),
transition: const NibTransition(duration: Duration(milliseconds: 200)),
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Hover Me',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
);
}
}While Drag
Makes a widget draggable and animates it while being dragged.
class WhileDragExample extends StatelessWidget {
const WhileDragExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotion(
drag: const NibDragConfig(),
whileDrag: const NibAnim(scale: 1.06, opacity: 0.85),
transition: NibTransition.springSnappy,
child: Container(
width: 180,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(80)),
),
child: const Center(
child: Text(
'Drag Me',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 16),
const Text(
'Drag anywhere',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
],
),
),
);
}
}Transition
Configures duration, curve, delay, and stagger for any animation.
NibMotion(
animate: NibAnim(opacity: 1),
transition: NibTransition.springSnappy,
child: MyWidget(),
)Spring Presets
Three physics spring presets — gentle, snappy, and wobbly — covering most common use cases.
class SpringPresetsExample extends StatefulWidget {
const SpringPresetsExample({super.key});
@override
State<SpringPresetsExample> createState() => _SpringPresetsExampleState();
}
class _SpringPresetsExampleState extends State<SpringPresetsExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_SpringCard(label: 'Gentle', transition: NibTransition.springGentle),
const SizedBox(width: 12),
_SpringCard(label: 'Snappy', transition: NibTransition.springSnappy),
const SizedBox(width: 12),
_SpringCard(label: 'Wobbly', transition: NibTransition.springWobbly),
],
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}
class _SpringCard extends StatelessWidget {
const _SpringCard({required this.label, required this.transition});
final String label;
final NibTransition transition;
@override
Widget build(BuildContext context) {
return NibMotion(
initial: const NibAnim(y: 60),
animate: const NibAnim(y: 0),
transition: transition,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(color: Color(0xFF718096), fontSize: 12),
),
],
),
);
}
}Tween Duration
Configure duration and easing curve for a classic tween-based transition.
class TweenDurationExample extends StatefulWidget {
const TweenDurationExample({super.key});
@override
State<TweenDurationExample> createState() => _TweenDurationExampleState();
}
class _TweenDurationExampleState extends State<TweenDurationExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_DurationCard(label: '200ms', duration: const Duration(milliseconds: 200)),
const SizedBox(width: 12),
_DurationCard(label: '600ms', duration: const Duration(milliseconds: 600)),
const SizedBox(width: 12),
_DurationCard(label: '1200ms', duration: const Duration(milliseconds: 1200)),
],
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}
class _DurationCard extends StatelessWidget {
const _DurationCard({required this.label, required this.duration});
final String label;
final Duration duration;
@override
Widget build(BuildContext context) {
return NibMotion(
initial: const NibAnim(opacity: 0),
animate: const NibAnim(opacity: 1),
transition: NibTransition(duration: duration, curve: Curves.easeOut),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(color: Color(0xFF718096), fontSize: 12),
),
],
),
);
}
}Delay & Stagger
Add a delay to offset when an animation starts, or staggerChildren to cascade across children.
class DelayStaggerExample extends StatefulWidget {
const DelayStaggerExample({super.key});
@override
State<DelayStaggerExample> createState() => _DelayStaggerExampleState();
}
class _DelayStaggerExampleState extends State<DelayStaggerExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_DelayItem(label: 'First — 0ms', delay: Duration.zero),
const SizedBox(height: 10),
_DelayItem(label: 'Second — 200ms', delay: const Duration(milliseconds: 200)),
const SizedBox(height: 10),
_DelayItem(label: 'Third — 400ms', delay: const Duration(milliseconds: 400)),
],
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}
class _DelayItem extends StatelessWidget {
const _DelayItem({required this.label, required this.delay});
final String label;
final Duration delay;
@override
Widget build(BuildContext context) {
return NibMotion(
initial: const NibAnim(opacity: 0, y: 20),
animate: const NibAnim(opacity: 1, y: 0),
transition: NibTransition(
spring: NibSpringDescription.gentle,
delay: delay,
),
child: Container(
width: 240,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(40)),
),
child: Center(
child: Text(
label,
style: const TextStyle(color: Color(0xFFEDF2F7), fontSize: 13),
),
),
),
);
}
}Rotate
Rotation-based transform animation, driven manually or via gesture.
NibMotion(
animate: NibAnim(rotate: 0.25), // 0.25 turns = 90°
transition: NibTransition.springSnappy,
child: MyWidget(),
)Rotate on Tap
Rotates 45° while pressed using whileTap, springs back on release.
class RotateOnTapExample extends StatelessWidget {
const RotateOnTapExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: NibMotion(
whileTap: const NibAnim(rotate: 0.125),
transition: NibTransition.springSnappy,
child: GestureDetector(
onTap: () {},
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(80)),
),
child: const Center(
child: Icon(Icons.star_rounded, color: Color(0xFF54C5F8), size: 40),
),
),
),
),
),
);
}
}Rotate Toggle
Animates between two rotation states — useful for expand/collapse chevrons.
class RotateToggleExample extends StatefulWidget {
const RotateToggleExample({super.key});
@override
State<RotateToggleExample> createState() => _RotateToggleExampleState();
}
class _RotateToggleExampleState extends State<RotateToggleExample> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: Container(
width: 240,
height: 56,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(40)),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Expand section',
style: TextStyle(color: Color(0xFFEDF2F7), fontSize: 14),
),
NibMotion(
animate: NibAnim(rotate: _expanded ? 0.5 : 0.0),
transition: NibTransition.springGentle,
child: const Icon(Icons.expand_more, color: Color(0xFF54C5F8)),
),
],
),
),
),
),
);
}
}Rotate Entrance
Spins in from −90° on mount. Uses springWobbly for a satisfying wind-up feel.
class RotateEntranceExample extends StatefulWidget {
const RotateEntranceExample({super.key});
@override
State<RotateEntranceExample> createState() => _RotateEntranceExampleState();
}
class _RotateEntranceExampleState extends State<RotateEntranceExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(rotate: -0.25),
animate: const NibAnim(rotate: 0),
transition: NibTransition.springWobbly,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(80)),
),
child: const Center(
child: Icon(Icons.auto_awesome, color: Color(0xFF54C5F8), size: 40),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Stagger
Staggers enter/exit animations across a list of children.
NibMotionList(
initial: NibAnim(opacity: 0, y: 32),
animate: NibAnim(opacity: 1, y: 0),
staggerDelay: Duration(milliseconds: 60),
children: items.map((item) => ItemCard(item)).toList(),
)Stagger List
Each child animates in with an offset delay — use NibMotionList for automatic staggering.
class StaggerListExample extends StatefulWidget {
const StaggerListExample({super.key});
@override
State<StaggerListExample> createState() => _StaggerListExampleState();
}
class _StaggerListExampleState extends State<StaggerListExample> {
final _signal = NibMotionReplaySignal();
static const _items = ['Spring physics', 'Gesture states', 'Exit animations', 'Viewport triggers'];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Column(
children: [
Expanded(
child: Center(
child: NibMotionReplayScope(
signal: _signal,
child: NibMotionList(
initial: const NibAnim(opacity: 0, y: 16),
animate: const NibAnim(opacity: 1, y: 0),
staggerDelay: const Duration(milliseconds: 80),
transition: NibTransition.springGentle,
children: _items
.map(
(label) => Padding(
key: ValueKey(label),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5),
child: Container(
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(30)),
),
child: Center(
child: Text(label, style: const TextStyle(color: Color(0xFFEDF2F7), fontSize: 14)),
),
),
),
)
.toList(),
),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
),
],
),
);
}
}Stagger Reverse
Reverse direction staggers from last child to first — useful for exit sequences.
class StaggerReverseExample extends StatefulWidget {
const StaggerReverseExample({super.key});
@override
State<StaggerReverseExample> createState() => _StaggerReverseExampleState();
}
class _StaggerReverseExampleState extends State<StaggerReverseExample> {
final _signal = NibMotionReplaySignal();
static const _items = ['First', 'Second', 'Third', 'Fourth'];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Column(
children: [
Expanded(
child: Center(
child: NibMotionReplayScope(
signal: _signal,
child: NibMotionList(
initial: const NibAnim(opacity: 0, y: 16),
animate: const NibAnim(opacity: 1, y: 0),
staggerDelay: const Duration(milliseconds: 100),
staggerDirection: NibStaggerDirection.reverse,
transition: NibTransition.springGentle,
children: _items
.map(
(label) => Padding(
key: ValueKey(label),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5),
child: Container(
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(30)),
),
child: Center(
child: Text(label, style: const TextStyle(color: Color(0xFFEDF2F7), fontSize: 14)),
),
),
),
)
.toList(),
),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
),
],
),
);
}
}Stagger In View
Stagger triggered when items enter the viewport — no manual scroll listener needed.
class StaggerInViewExample extends StatelessWidget {
const StaggerInViewExample({super.key});
static const _items = [
'Scroll to reveal',
'Each item fades',
'In staggered order',
'Using whileInView',
'With per-item delay',
'No scroll listener',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
itemCount: _items.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: NibMotion(
initial: const NibAnim(opacity: 0, y: 20),
whileInView: const NibAnim(opacity: 1, y: 0),
viewport: const NibInViewConfig(amount: 0.3, once: false),
transition: NibTransition(
spring: NibSpringDescription.gentle,
delay: Duration(milliseconds: index * 60),
),
child: Container(
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(30)),
),
child: Center(
child: Text(_items[index], style: const TextStyle(color: Color(0xFFEDF2F7), fontSize: 14)),
),
),
),
),
),
);
}
}In View
Triggers an animation when a widget enters the viewport.
NibMotion(
initial: NibAnim(opacity: 0, y: 24),
whileInView: NibAnim(opacity: 1, y: 0),
transition: NibTransition.springGentle,
child: MyWidget(),
)Fade In View
Fades in when the widget enters the viewport. Replays each time it enters.
class InViewFadeExample extends StatelessWidget {
const InViewFadeExample({super.key});
static const _labels = [
'Scroll down',
'Each card fades',
'As it enters view',
'Opacity only',
'Replays on re-enter',
'whileInView',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
itemCount: _labels.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: NibMotion(
initial: const NibAnim(opacity: 0),
whileInView: const NibAnim(opacity: 1),
viewport: const NibInViewConfig(amount: 0.3, once: false),
transition: const NibTransition(duration: Duration(milliseconds: 500)),
child: Container(
height: 56,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(30)),
),
child: Center(
child: Text(_labels[index], style: const TextStyle(color: Color(0xFFEDF2F7), fontSize: 14)),
),
),
),
),
),
);
}
}Slide In View
Slides up from below when scrolled into view.
class InViewSlideExample extends StatelessWidget {
const InViewSlideExample({super.key});
static const _labels = [
'Scroll down',
'Each card slides up',
'Y axis only',
'No opacity change',
'Spring physics',
'whileInView',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
itemCount: _labels.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: NibMotion(
initial: const NibAnim(y: 32),
whileInView: const NibAnim(y: 0),
viewport: const NibInViewConfig(amount: 0.3, once: false),
transition: NibTransition.springGentle,
child: Container(
height: 56,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(30)),
),
child: Center(
child: Text(_labels[index], style: const TextStyle(color: Color(0xFFEDF2F7), fontSize: 14)),
),
),
),
),
),
);
}
}Animate Once
Trigger the animation only the first time the widget enters the viewport.
class InViewOnceExample extends StatelessWidget {
const InViewOnceExample({super.key});
static const _labels = [
'Scroll down',
'Animates once only',
'Scroll back up',
'Then back down',
"Won't replay",
'once: true',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
itemCount: _labels.length,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: NibMotion(
initial: const NibAnim(opacity: 0),
whileInView: const NibAnim(opacity: 1),
viewport: const NibInViewConfig(amount: 0.3, once: true),
transition: const NibTransition(duration: Duration(milliseconds: 500)),
child: Container(
height: 56,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(30)),
),
child: Center(
child: Text(_labels[index], style: const TextStyle(color: Color(0xFFEDF2F7), fontSize: 14)),
),
),
),
),
),
);
}
}Spring
Physics-based spring motion driven by mass, stiffness, and damping.
NibMotion(
initial: NibAnim(opacity: 0, y: 16),
animate: NibAnim(opacity: 1, y: 0),
transition: NibTransition(
spring: NibSpringDescription.wobbly,
),
child: MyWidget(),
)Spring Entrance
Physics-based spring entrance using mass, stiffness, and damping instead of a fixed duration.
class SpringEntranceExample extends StatefulWidget {
const SpringEntranceExample({super.key});
@override
State<SpringEntranceExample> createState() => _SpringEntranceExampleState();
}
class _SpringEntranceExampleState extends State<SpringEntranceExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(y: 60),
animate: const NibAnim(y: 0),
transition: NibTransition.springGentle,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Spring Gentle',
style: TextStyle(color: Color(0xFFEDF2F7), fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Wobbly Bounce
springWobbly overshoots and oscillates — great for playful UI like FABs or confirmations.
class WobblyBounceExample extends StatefulWidget {
const WobblyBounceExample({super.key});
@override
State<WobblyBounceExample> createState() => _WobblyBounceExampleState();
}
class _WobblyBounceExampleState extends State<WobblyBounceExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(scale: 0.0),
animate: const NibAnim(scale: 1.0),
transition: NibTransition.springWobbly,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF54C5F8),
borderRadius: BorderRadius.circular(20),
),
child: const Center(
child: Icon(Icons.check_rounded, color: Color(0xFF07080D), size: 40),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Custom Spring
Full control over mass, stiffness, and damping with NibSpringDescription.
class CustomSpringExample extends StatefulWidget {
const CustomSpringExample({super.key});
@override
State<CustomSpringExample> createState() => _CustomSpringExampleState();
}
class _CustomSpringExampleState extends State<CustomSpringExample> {
final _signal = NibMotionReplaySignal();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotionReplayScope(
signal: _signal,
child: NibMotion(
initial: const NibAnim(y: 80),
animate: const NibAnim(y: 0),
transition: const NibTransition(
spring: NibSpringDescription(
mass: 1.0,
stiffness: 200.0,
damping: 16.0,
),
),
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Custom Spring',
style: TextStyle(color: Color(0xFFEDF2F7), fontSize: 15, fontWeight: FontWeight.w600),
),
),
),
),
),
const SizedBox(height: 8),
const Text(
'mass: 1.0 stiffness: 200 damping: 16',
style: TextStyle(color: Color(0xFF718096), fontSize: 11, fontFamily: 'monospace'),
),
const SizedBox(height: 16),
TextButton.icon(
onPressed: _signal.replay,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Text
Animate text per character, word, or line, sweep gradients across glyphs, reveal blocks with a clip-mask wipe, or type text out like a typewriter.
NibText(
'Hello world',
config: NibTextConfig(
animate: NibAnim(opacity: 1, y: 0),
initial: NibAnim(opacity: 0, y: 12),
),
)Gradient Sweep
An animated gradient sweeps across the glyphs of static text via a srcIn mask — the text itself never moves.
class TextGradientExample extends StatelessWidget {
const TextGradientExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: NibGradientText(
'Gradient Sweep',
gradient: const LinearGradient(
colors: [Color(0xFF54C5F8), Color(0xFFEDF2F7), Color(0xFF54C5F8)],
),
duration: const Duration(milliseconds: 2000),
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w700),
),
),
);
}
}Typewriter
Characters are revealed one at a time at a fixed speed, with a blinking cursor — the one widget in the text system that legitimately uses setState.
class TypewriterExample extends StatefulWidget {
const TypewriterExample({super.key});
@override
State<TypewriterExample> createState() => _TypewriterExampleState();
}
class _TypewriterExampleState extends State<TypewriterExample> {
int _replayCount = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
KeyedSubtree(
key: ValueKey(_replayCount),
child: NibTypewriter(
'Typed one character at a time.',
speed: const Duration(milliseconds: 50),
cursor: '|',
repeat: true,
style: const TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: () => setState(() => _replayCount++),
icon: const Icon(
Icons.replay,
size: 16,
color: Color(0xFF718096),
),
label: const Text(
'Replay',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
),
],
),
),
);
}
}Text Reveal
A block of text wipes into view via an animated clip mask, with no per-character splitting.
class TextRevealExample extends StatefulWidget {
const TextRevealExample({super.key});
@override
State<TextRevealExample> createState() => _TextRevealExampleState();
}
class _TextRevealExampleState extends State<TextRevealExample> {
int _replayCount = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
KeyedSubtree(
key: ValueKey(_replayCount),
child: NibTextReveal(
direction: NibTextRevealDirection.left,
duration: const Duration(milliseconds: 700),
child: Container(
width: 220,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF54C5F8).withAlpha(50),
),
),
child: const Center(
child: Text(
'Text Reveal',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: () => setState(() => _replayCount++),
icon: const Icon(
Icons.replay,
size: 16,
color: Color(0xFF718096),
),
label: const Text(
'Replay',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
),
],
),
),
);
}
}Shaders
GPU-driven fragment-shader effects — a noise-pattern dissolve transition (NibDissolve) and a diagonal shimmer sweep (NibShimmer) for skeleton loaders, both painted directly via RenderObject with no per-frame widget rebuild.
NibDissolve(
direction: NibDissolveDirection.outward,
duration: const Duration(milliseconds: 500),
child: myWidget,
)Dissolve
Noise-pattern fragment shader transition — a textured, GPU-driven alternative to a plain opacity fade.
class DissolveExample extends StatefulWidget {
const DissolveExample({super.key});
@override
State<DissolveExample> createState() => _DissolveExampleState();
}
class _DissolveExampleState extends State<DissolveExample> {
bool _dissolved = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibDissolve(
key: ValueKey(_dissolved),
direction: _dissolved
? NibDissolveDirection.outward
: NibDissolveDirection.inward,
duration: const Duration(milliseconds: 700),
noiseScale: 0.02,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Dissolve',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: () => setState(() => _dissolved = !_dissolved),
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: Text(
_dissolved ? 'Dissolve In' : 'Dissolve Out',
style: const TextStyle(color: Color(0xFF718096), fontSize: 13),
),
),
],
),
),
);
}
}Shimmer
GPU-accelerated diagonal shimmer sweep — a drop-in replacement for CSS-style skeleton loaders.
class ShimmerExample extends StatelessWidget {
const ShimmerExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: NibShimmer(
baseColor: const Color(0xFF1C2537),
shimmerColor: const Color(0xFF54C5F8),
speed: 1.5,
angle: -0.3,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Loading…',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
);
}
}Presets
A library of ready-made animation widgets — NibBounce, NibShake, NibRubberBand, NibFloat, NibPulse, and NibFlip — for common UI motion patterns without hand-writing keyframes.
NibBounce(
child: const Text('Welcome!'),
)Bounce + Shake
NibBounce plays a drop-in entrance automatically; NibShake plays a validation-style shake on demand via NibShakeController.
class BounceShakeExample extends StatefulWidget {
const BounceShakeExample({super.key});
@override
State<BounceShakeExample> createState() => _BounceShakeExampleState();
}
class _BounceShakeExampleState extends State<BounceShakeExample> {
final _shakeController = NibShakeController();
@override
void dispose() {
_shakeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibBounce(
child: Container(
width: 200,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Bounce In',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 24),
NibShake(
controller: _shakeController,
child: Container(
width: 200,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Invalid input',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _shakeController.shake,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Shake', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Rubber Band
An overshoot-and-settle stretch animation, replayable on tap.
class RubberBandExample extends StatefulWidget {
const RubberBandExample({super.key});
@override
State<RubberBandExample> createState() => _RubberBandExampleState();
}
class _RubberBandExampleState extends State<RubberBandExample> {
int _tick = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibRubberBand(
key: ValueKey('rubber-band-$_tick'),
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Stretchy!',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: () => setState(() => _tick++),
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Float + Pulse
NibFloat and NibPulse both loop forever — gentle ambient motion with no user interaction required.
class FloatPulseExample extends StatelessWidget {
const FloatPulseExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
NibFloat(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Icon(Icons.cloud_outlined, color: Color(0xFF54C5F8), size: 32),
),
),
),
const SizedBox(width: 24),
NibPulse(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Icon(Icons.favorite, color: Color(0xFF54C5F8), size: 32),
),
),
),
],
),
),
);
}
}Drag & Pinch
Standalone drag, pinch-to-zoom, and swipe-to-dismiss widgets with bounds, axis locking, and spring snap-back — independent of NibMotion's animate/variants system.
NibDraggable(
bounds: const Rect.fromLTRB(-80, -45, 80, 45),
snapBack: true,
child: MyCard(),
)Draggable
A NibDraggable wrapper with axis-free bounds and spring snap-back to the origin on release.
class DraggableExample extends StatefulWidget {
const DraggableExample({super.key});
@override
State<DraggableExample> createState() => _DraggableExampleState();
}
class _DraggableExampleState extends State<DraggableExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 240,
height: 150,
decoration: BoxDecoration(
color: const Color(0xFF1C2537).withAlpha(60),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(30)),
),
child: Center(
child: NibDraggable(
bounds: const Rect.fromLTRB(-80, -45, 80, 45),
snapBack: true,
spring: NibSpringDescription.gentle,
child: Container(
width: 150,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'drag me',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
),
const SizedBox(height: 24),
const Text(
'bounds + snapBack',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
],
),
),
);
}
}Pinch to Zoom
NibPinch clamps the scale between minScale/maxScale and springs back to initialScale on release.
class PinchExample extends StatefulWidget {
const PinchExample({super.key});
@override
State<PinchExample> createState() => _PinchExampleState();
}
class _PinchExampleState extends State<PinchExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibPinch(
minScale: 0.5,
maxScale: 4.0,
snapBackOnRelease: true,
spring: NibSpringDescription.gentle,
child: Container(
width: 160,
height: 160,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'pinch me',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
),
const SizedBox(height: 24),
const Text(
'minScale: 0.5 maxScale: 4.0',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
],
),
),
);
}
}Swipe to Dismiss
NibSwipeDismiss on a list-item card — swipe past the threshold to dismiss, or release early to spring back.
class SwipeDismissExample extends StatefulWidget {
const SwipeDismissExample({super.key});
@override
State<SwipeDismissExample> createState() => _SwipeDismissExampleState();
}
class _SwipeDismissExampleState extends State<SwipeDismissExample> {
bool _dismissed = false;
void _reset() => setState(() => _dismissed = false);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 260,
height: 80,
child: _dismissed
? Center(
child: TextButton.icon(
onPressed: _reset,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Reset', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
)
: NibSwipeDismiss(
dismissDirection: DismissDirection.horizontal,
dismissThreshold: 0.4,
spring: NibSpringDescription.gentle,
onDismissed: () => setState(() => _dismissed = true),
background: Container(
decoration: BoxDecoration(
color: const Color(0xFFEF4444).withAlpha(60),
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: const Icon(Icons.delete_outline, color: Color(0xFFEDF2F7)),
),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'swipe to dismiss',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
const SizedBox(height: 24),
const Text(
'dismissThreshold: 0.4',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
],
),
),
);
}
}Timeline
Imperative orchestration of multiple NibMotionControllers against a single shared clock, plus a fluent per-controller animate/wait/loop sequence builder.
final timeline = NibTimeline([
NibTimelineEntry(controller: a, target: const NibAnim(opacity: 1), at: Duration.zero),
NibTimelineEntry(controller: b, target: const NibAnim(opacity: 1), at: const Duration(milliseconds: 150)),
]);
await timeline.play();Basic Timeline
Three controllers fired off a single shared clock using `at:` offsets — a declarative playbook for a multi-element entrance.
class TimelineBasicExample extends StatefulWidget {
const TimelineBasicExample({super.key});
@override
State<TimelineBasicExample> createState() => _TimelineBasicExampleState();
}
class _TimelineBasicExampleState extends State<TimelineBasicExample> {
final _titleController = NibMotionController();
final _subtitleController = NibMotionController();
NibTimeline? _timeline;
void _play() {
_titleController.set(const NibAnim(opacity: 0, y: 16));
_subtitleController.set(const NibAnim(opacity: 0, y: 16));
_timeline?.dispose();
_timeline = NibTimeline([
NibTimelineEntry(
controller: _titleController,
target: const NibAnim(opacity: 1, y: 0),
at: Duration.zero,
),
NibTimelineEntry(
controller: _subtitleController,
target: const NibAnim(opacity: 1, y: 0),
at: const Duration(milliseconds: 150),
),
]);
_timeline!.play();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _play());
}
@override
void dispose() {
_timeline?.dispose();
_titleController.dispose();
_subtitleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotion(
initial: const NibAnim(opacity: 0, y: 16),
controller: _titleController,
transition: const NibTransition(
duration: Duration(milliseconds: 300),
),
child: const Text(
'Title',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(height: 8),
NibMotion(
initial: const NibAnim(opacity: 0, y: 16),
controller: _subtitleController,
transition: const NibTransition(
duration: Duration(milliseconds: 300),
),
child: const Text(
'Subtitle follows 150ms later',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 14),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _play,
icon: const Icon(
Icons.play_arrow,
size: 16,
color: Color(0xFF718096),
),
label: const Text(
'Play',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
),
],
),
),
);
}
}Animation Sequence
A single controller driven through a chained animate/wait sequence via the fluent NibAnimationSequence builder.
class AnimationSequenceExample extends StatefulWidget {
const AnimationSequenceExample({super.key});
@override
State<AnimationSequenceExample> createState() =>
_AnimationSequenceExampleState();
}
class _AnimationSequenceExampleState extends State<AnimationSequenceExample> {
final _controller = NibMotionController();
void _play() {
_controller.set(const NibAnim(y: 0, opacity: 1, scale: 1));
NibAnimationSequence(_controller)
.animate(
const NibAnim(y: -20, opacity: 0),
transition: const NibTransition(
duration: Duration(milliseconds: 200),
),
)
.wait(const Duration(milliseconds: 100))
.animate(
const NibAnim(y: 0, opacity: 1, scale: 1.1),
transition: NibTransition.springSnappy,
)
.wait(const Duration(milliseconds: 150))
.animate(
const NibAnim(scale: 1),
transition: NibTransition.springGentle,
)
.play();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _play());
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotion(
controller: _controller,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF54C5F8).withAlpha(50),
),
),
child: const Center(
child: Text(
'Sequence',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _play,
icon: const Icon(
Icons.play_arrow,
size: 16,
color: Color(0xFF718096),
),
label: const Text(
'Play',
style: TextStyle(color: Color(0xFF718096), fontSize: 13),
),
),
],
),
),
);
}
}Path Motion
Moves or draws a widget along a custom Path — for curved motion trajectories or signature/SVG-style line-draw reveals.
NibPathMotion(
path: myPath,
duration: const Duration(milliseconds: 1000),
curve: Curves.easeInOut,
rotate: true,
child: MyWidget(),
)Path Motion
Moves a widget along a custom cubic Bezier Path, rotating to follow the tangent direction.
class PathMotionExample extends StatefulWidget {
const PathMotionExample({super.key});
@override
State<PathMotionExample> createState() => _PathMotionExampleState();
}
class _PathMotionExampleState extends State<PathMotionExample> {
final _controller = NibPathMotionController();
Path _buildPath(Size size) {
final path = Path();
path.moveTo(0, size.height);
path.quadraticBezierTo(size.width * 0.5, 0, size.width, size.height);
return path;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const trackSize = Size(220, 100);
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: trackSize.width,
height: trackSize.height,
child: NibPathMotion(
controller: _controller,
path: _buildPath(trackSize),
duration: const Duration(milliseconds: 1400),
curve: Curves.easeInOut,
rotate: true,
child: const Icon(Icons.flight, color: Color(0xFF54C5F8), size: 28),
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _controller.restart,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Draw Path
Animates the progressive reveal of a stroked Path, like a signature or SVG line draw.
class DrawPathExample extends StatefulWidget {
const DrawPathExample({super.key});
@override
State<DrawPathExample> createState() => _DrawPathExampleState();
}
class _DrawPathExampleState extends State<DrawPathExample> {
final _controller = NibDrawPathController();
Path _buildPath(Size size) {
final path = Path();
path.moveTo(size.width * 0.05, size.height * 0.6);
path.cubicTo(
size.width * 0.3,
0,
size.width * 0.7,
size.height,
size.width * 0.95,
size.height * 0.4,
);
return path;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const trackSize = Size(220, 100);
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: trackSize.width,
height: trackSize.height,
child: NibDrawPath(
controller: _controller,
path: _buildPath(trackSize),
strokeWidth: 3,
color: const Color(0xFF54C5F8),
duration: const Duration(milliseconds: 1200),
curve: Curves.easeOut,
),
),
const SizedBox(height: 24),
TextButton.icon(
onPressed: _controller.restart,
icon: const Icon(Icons.replay, size: 16, color: Color(0xFF718096)),
label: const Text('Replay', style: TextStyle(color: Color(0xFF718096), fontSize: 13)),
),
],
),
),
);
}
}Parallax
Moves a child at a different rate than the surrounding scroll view for a depth effect, and reveals widgets with an animation as they enter the viewport.
NibParallax(
scrollController: myScrollController,
factor: 0.5,
child: const Image(image: AssetImage('background.png')),
)Parallax Layers
Background layer moves at half the scroll rate via NibParallax's factor, creating a depth effect.
class ParallaxExample extends StatefulWidget {
const ParallaxExample({super.key});
@override
State<ParallaxExample> createState() => _ParallaxExampleState();
}
class _ParallaxExampleState extends State<ParallaxExample> {
final _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: Stack(
children: [
NibParallax(
scrollController: _scrollController,
factor: 0.5,
child: Container(
height: 800,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF1C2537),
const Color(0xFF54C5F8).withAlpha(40),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
),
ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(24),
itemCount: 8,
itemBuilder: (context, index) => Container(
margin: const EdgeInsets.only(bottom: 16),
height: 80,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: Center(
child: Text(
'Foreground ${index + 1}',
style: const TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
),
],
),
);
}
}Scroll Reveal
Cards fade and slide up as they enter the viewport, using NibScrollReveal.
class ScrollRevealExample extends StatelessWidget {
const ScrollRevealExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: ListView.builder(
padding: const EdgeInsets.all(24),
itemCount: 8,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: NibScrollReveal(
amount: 0.3,
child: Container(
height: 90,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: Center(
child: Text(
'Revealed ${index + 1}',
style: const TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
},
),
);
}
}Debugger
A conditional debug overlay showing a live FPS counter and a scrollable list of every registered MotionValue — zero-overhead no-op passthrough when disabled.
NibMotionDebugger(
enabled: kDebugMode,
child: MyApp(),
)Basic Debugger Overlay
Wrap any subtree in NibMotionDebugger to get a live FPS counter and a scrollable list of every registered MotionValue while developing.
class DebuggerBasicExample extends StatefulWidget {
const DebuggerBasicExample({super.key});
@override
State<DebuggerBasicExample> createState() => _DebuggerBasicExampleState();
}
class _DebuggerBasicExampleState extends State<DebuggerBasicExample> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1117),
body: NibMotionDebugger(
enabled: true,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NibMotion(
animate: NibAnim(opacity: _visible ? 1 : 0.3),
transition: const NibTransition(duration: Duration(milliseconds: 300)),
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: const Color(0xFF1C2537),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF54C5F8).withAlpha(50)),
),
child: const Center(
child: Text(
'Watch the panel',
style: TextStyle(
color: Color(0xFFEDF2F7),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
),
const SizedBox(height: 24),
TextButton(
onPressed: () => setState(() => _visible = !_visible),
child: Text(
_visible ? 'Fade Out' : 'Fade In',
style: const TextStyle(color: Color(0xFF718096)),
),
),
],
),
),
),
);
}
}