Text

Text deserves its own motion vocabulary. A heading that types itself out, a hero title where each word drops in a beat after the last, a gradient that sweeps endlessly across a label — these are common enough patterns that nib_motion ships four purpose-built widgets for them instead of asking you to hand-roll per-character animation on top of NibMotion yourself.

NibText (with NibTextConfig) handles per-character, per-word, or per-line staggered entrances and exits. NibGradientText sweeps an animated gradient across static text. NibTextReveal wipes a block of text into view with a clip mask. NibTypewriter reveals text one character at a time, optionally with a blinking cursor. Each is independent — pick whichever matches the effect you’re after.

NibText: staggered character, word, and line animation

NibText splits its string into units — characters, words, or lines, depending on NibTextConfig.unit — and wraps each animated unit in its own NibMotion, staggered in by NibTextConfig.staggerDelay. It reuses the same zero-setState NibMotion engine the rest of the package is built on; the splitting and staggering logic is the only thing NibText adds.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class FadeInHeadline extends StatelessWidget {
  const FadeInHeadline({super.key});
 
  @override
  Widget build(BuildContext context) {
    return NibText(
      'Build interfaces that move.',
      style: Theme.of(context).textTheme.headlineMedium,
      config: const NibTextConfig(
        unit: NibTextUnit.word,
        initial: NibAnim(opacity: 0, y: 12),
        animate: NibAnim(opacity: 1, y: 0),
        staggerDelay: Duration(milliseconds: 40),
      ),
    );
  }
}

Each word fades and slides up into place, one staggerDelay after the previous word started. Whitespace runs between words are preserved as plain, unanimated Text so the layout reads naturally — only the words themselves are wrapped in NibMotion.

Choosing a unit

NibTextConfig.unit is a NibTextUnit: character (the default), word, or line. character splits on grapheme clusters rather than raw UTF-16 code units, so multi-code-point emoji and combining marks stay intact as a single animated piece. line splits on \n and lays the result out in a Column instead of a Wrap — useful for a multi-line headline where each line arrives as a whole.

NibText(
  'One line at a time.\nThen the next.',
  config: const NibTextConfig(
    unit: NibTextUnit.line,
    initial: NibAnim(opacity: 0, x: -16),
    animate: NibAnim(opacity: 1, x: 0),
    staggerDelay: Duration(milliseconds: 120),
  ),
)

staggerFrom: where the delay accumulates from

NibTextConfig.staggerFrom (a NibTextStaggerFrom) controls the order units animate in, independent of the order they’re laid out in:

ValueBehavior
startDelay increases from the first unit to the last (the default).
endDelay increases from the last unit to the first — the end of the text animates in first.
centerDelay increases outward from the middle unit toward both ends.
randomDelay is a seeded-random value within the same overall window, so units arrive in a shuffled order.
NibText(
  'CENTER OUT',
  config: const NibTextConfig(
    unit: NibTextUnit.character,
    initial: NibAnim(opacity: 0, scale: 0.5),
    animate: NibAnim(opacity: 1, scale: 1),
    staggerDelay: Duration(milliseconds: 25),
    staggerFrom: NibTextStaggerFrom.center,
  ),
)

Exit animations

Setting NibTextConfig.exit wraps each animated unit in its own NibPresence, so removing the NibText from the tree (inside an ancestor NibPresence) plays every unit’s exit animation before it actually unmounts — the same per-unit stagger applies on the way out as on the way in.

NibText(
  'Saved!',
  config: const NibTextConfig(
    unit: NibTextUnit.character,
    initial: NibAnim(opacity: 0),
    animate: NibAnim(opacity: 1),
    exit: NibAnim(opacity: 0, y: -8),
    staggerDelay: Duration(milliseconds: 20),
  ),
)

NibText.rich

NibText.rich takes an InlineSpan (such as a TextSpan tree) instead of a flat String — its toPlainText() is split into units the same way, with style used as the shared base style for every unit. Per-span styling inside the original InlineSpan is not preserved per unit.

NibGradientText: a sweeping gradient across static text

NibGradientText paints text once, then sweeps an animated Gradient across it by masking the gradient onto the rendered glyphs. The text itself never moves — only the gradient’s position animates — which makes this a cheap way to add a shimmer or shine effect to a label or heading.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class ShimmeringTitle extends StatelessWidget {
  const ShimmeringTitle({super.key});
 
  @override
  Widget build(BuildContext context) {
    return NibGradientText(
      'Premium',
      style: Theme.of(context).textTheme.headlineSmall,
      gradient: const LinearGradient(
        colors: [Color(0xFF54C5F8), Color(0xFF02569B), Color(0xFF54C5F8)],
      ),
      duration: const Duration(milliseconds: 2200),
    );
  }
}

By default the sweep loops forever (repeat: true), travelling left to right and wrapping back to the opposite edge each cycle. Set repeat: false to play the sweep once and stop, or direction: TextDirection.rtl to reverse it. style’s color is ignored — the gradient is what tints the glyphs, via a BlendMode.srcIn mask applied to the painted text.

NibTextReveal: a clip-mask wipe

NibTextReveal reveals its child — typically a block of text, but any widget works — by animating a clip mask that wipes in from one edge. Unlike NibText, there’s no per-character splitting: the whole block reveals at once, as if a panel were sliding away to expose it.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class WipeInTitle extends StatelessWidget {
  const WipeInTitle({super.key});
 
  @override
  Widget build(BuildContext context) {
    return NibTextReveal(
      direction: NibTextRevealDirection.left,
      duration: const Duration(milliseconds: 600),
      curve: Curves.easeOutCubic,
      child: Text(
        'Welcome back',
        style: Theme.of(context).textTheme.headlineMedium,
      ),
    );
  }
}

direction (a NibTextRevealDirection) is one of left, right, up, or down, describing the edge the reveal wipes in fromleft clips the content starting at its left edge and grows rightward until it’s fully visible. Pair delay with a controller (NibMotionController) and call controller.start() to replay the wipe on demand, such as when a card scrolls into view.

NibTextReveal(
  direction: NibTextRevealDirection.up,
  delay: const Duration(milliseconds: 200),
  duration: const Duration(milliseconds: 500),
  child: const Text('Now appearing from below'),
)

NibTypewriter: classic character-by-character reveal

NibTypewriter reveals text one grapheme at a time at a fixed speed, optionally with a blinking cursor. This is the one text widget in nib_motion that uses setState internally rather than a MotionValue — the revealed character count is a discrete, content-level value advanced by a timer, not a continuous per-frame paint value, so there’s no animation machinery to bypass.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class TerminalGreeting extends StatelessWidget {
  const TerminalGreeting({super.key});
 
  @override
  Widget build(BuildContext context) {
    return NibTypewriter(
      'Initializing system...',
      speed: const Duration(milliseconds: 45),
      cursor: '_',
      style: const TextStyle(fontFamily: 'monospace', fontSize: 16),
      onComplete: () => debugPrint('typing finished'),
    );
  }
}

Set repeat: true to loop forever — once typing finishes, NibTypewriter pauses briefly, deletes the text at deleteSpeed (or speed if unset), and types it again:

NibTypewriter(
  'Looping forever...',
  speed: const Duration(milliseconds: 60),
  deleteSpeed: const Duration(milliseconds: 30),
  repeat: true,
  cursor: '|',
)

delay defers the start of typing — useful for sequencing a typewriter effect after some other entrance animation has finished. onComplete fires exactly once, the first time the full text is revealed, even across later repeat cycles.

API reference

NibText

PropTypeDefaultDescription
(constructor)NibText(String text, {Key? key, NibTextConfig config, TextStyle? style, TextAlign? textAlign, TextOverflow? overflow, NibMotionController? controller})Creates a NibText that animates text split according to config.unit.
NibText.richNibText.rich(InlineSpan span, {Key? key, NibTextConfig config, TextStyle? style, TextAlign? textAlign, TextOverflow? overflow, NibMotionController? controller})Creates a NibText from a rich InlineSpan; span.toPlainText() is split into units and style is used as each unit’s base style.
configNibTextConfigNibTextConfig()Configures how the text is split into units and how each unit animates.
styleTextStyle?nullThe base text style applied to every unit.
textAlignTextAlign?nullText alignment for the laid-out units.
overflowTextOverflow?nullHow to visually overflow text that doesn’t fit, applied to each unit’s internal Text.
controllerNibMotionController?nullForwarded to every unit’s NibMotion.controller, so a single NibMotionController can imperatively drive every unit at once.

NibTextConfig

PropTypeDefaultDescription
unitNibTextUnitNibTextUnit.characterWhether to animate per-character, per-word, or per-line.
initialNibAnim?null (falls back to NibAnim.identity)The starting state of each unit before its entrance.
animateNibAnim?nullThe target state of each unit at rest. When null, units render at initial with no entrance animation.
exitNibAnim?nullThe state each unit transitions to when the widget exits via an ancestor NibPresence.
staggerDelayDurationDuration(milliseconds: 30)The delay between each successive unit’s animation start.
staggerFromNibTextStaggerFromNibTextStaggerFrom.startWhere stagger delay accumulates from — start, end, center, or random.
transitionNibTransition?null (falls back to a 400ms tween)The transition used for each unit’s entrance animation.

NibTextUnit

ValueDescription
characterEach grapheme cluster (user-perceived character, including multi-code-unit emoji) is its own animated unit.
wordEach whitespace-delimited word is its own animated unit. Whitespace runs between words are preserved as plain, non-animated text.
lineEach line (split on \n) is its own animated unit.

NibTextStaggerFrom

ValueDescription
startDelay increases from the first unit to the last: index * staggerDelay.
endDelay increases from the last unit to the first: (totalUnits - 1 - index) * staggerDelay.
centerDelay increases outward from the middle unit toward both ends.
randomDelay is a seeded-random value in [0, totalUnits * staggerDelay), so units animate in over the same overall window but in a shuffled order.

NibGradientText

PropTypeDefaultDescription
(constructor)NibGradientText(String text, {Key? key, required Gradient gradient, TextStyle? style, Duration duration, bool repeat, TextDirection direction})Creates a NibGradientText that sweeps gradient across text.
textStringThe text to render.
gradientGradientThe gradient swept across text.
styleTextStyle?nullThe base text style. Its color is ignored — the gradient masks the glyphs instead.
durationDurationDuration(milliseconds: 2000)How long one full sweep across the text takes.
repeatbooltrueWhether the sweep repeats forever (true) or plays once and stops (false).
directionTextDirectionTextDirection.ltrThe direction the sweep travels: left-to-right or right-to-left.

NibTextReveal

PropTypeDefaultDescription
(constructor)NibTextReveal({Key? key, required Widget child, NibTextRevealDirection direction, Duration duration, Duration delay, Curve curve, NibMotionController? controller})Creates a NibTextReveal that wipes child into view from direction.
childWidgetThe content being revealed.
directionNibTextRevealDirectionNibTextRevealDirection.leftThe edge the reveal wipes in from.
durationDurationDuration(milliseconds: 500)How long the reveal animation takes.
delayDurationDuration.zeroHow long to wait before the reveal starts.
curveCurveCurves.easeInOutThe easing curve for the reveal.
controllerNibMotionController?nullAn optional external handle for replaying the reveal via NibMotionController.start.

NibTextRevealDirection

ValueDescription
leftReveals left-to-right, clipping from the left edge.
rightReveals right-to-left, clipping from the right edge.
upReveals bottom-to-top, clipping from the top edge.
downReveals top-to-bottom, clipping from the bottom edge.

NibTypewriter

PropTypeDefaultDescription
(constructor)NibTypewriter(String text, {Key? key, Duration speed, String? cursor, Duration cursorBlinkInterval, TextStyle? style, VoidCallback? onComplete, bool repeat, Duration? deleteSpeed, Duration delay})Creates a typewriter effect that reveals text over time.
textStringThe full text to reveal.
speedDurationDuration(milliseconds: 50)How long each newly revealed character stays before the next one appears.
cursorString?'|'The cursor string appended while typing (and, if repeat, while deleting). null disables the cursor entirely.
cursorBlinkIntervalDurationDuration(milliseconds: 530)How fast the cursor blinks once typing has paused. Ignored if cursor is null.
styleTextStyle?nullThe text style applied to the revealed text and cursor.
onCompleteVoidCallback?nullCalled once, when the full text has been revealed. Not called again on subsequent repeat cycles.
repeatboolfalseIf true, after fully typing text the widget deletes it (at deleteSpeed) and types it again, forever.
deleteSpeedDuration?null (falls back to speed)How fast characters are deleted during a repeat cycle.
delayDurationDuration.zeroHow long to wait before typing starts.

Next steps

  • AnimateNibText’s per-unit entrances are built on the same NibAnim/NibTransition model that drives every other NibMotion in the package.
  • Lists and staggerNibText’s staggerDelay/staggerFrom mirror the stagger config used for animating lists of widgets, if you want the same timing language in both places.
  • Transitions — for the curve/spring options available to NibTextConfig.transition and NibTextReveal.curve.