Shaders

NibMotion’s core values — opacity, position, scale, color — cover most transitions, but some effects are easiest to describe as a per-pixel operation rather than an interpolated property. nib_motion’s shader-based widgets, NibDissolve and NibShimmer, fill that gap: a textured dissolve transition and a skeleton-loading sweep, both painted directly over your widget’s rasterized content.

Despite the name, neither widget requires you to write or bundle a GLSL fragment shader yourself — both ship with a bundled, precompiled shader program that nib_motion loads internally. If that shader program hasn’t finished compiling yet (or fails to compile in the host environment), each widget falls back to a plain, correct-looking alternative — an opacity fade for NibDissolve, no overlay at all for NibShimmer — so a slow or failed shader load never leaves the UI broken.

NibDissolve: a textured dissolve transition

NibDissolve dissolves its child in or out using a noise pattern instead of a uniform opacity fade — a drop-in look-upgrade for FadeTransition in page or element transitions that want a more textured, “burning away” feel.

Internally, the running widget needs a real, currently-visible child to animate: on the frame the dissolve starts, NibDissolve snapshots child’s painted output into a ui.Image (via the same machinery as RenderRepaintBoundary.toImage()) and reuses that single snapshot for the rest of the dissolve cycle, shading it with the dissolve shader as progress advances. This means the dissolving content is expected to be static for the duration of one dissolve — NibDissolve isn’t meant to wrap something that’s simultaneously animating its own layout or content.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class DissolveCard extends StatefulWidget {
  const DissolveCard({super.key});
 
  @override
  State<DissolveCard> createState() => _DissolveCardState();
}
 
class _DissolveCardState extends State<DissolveCard> {
  bool _visible = true;
 
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (_visible)
          NibDissolve(
            direction: NibDissolveDirection.outward,
            duration: const Duration(milliseconds: 600),
            onComplete: () => setState(() => _visible = false),
            child: Container(
              width: 220,
              height: 140,
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.primary,
                borderRadius: BorderRadius.circular(12),
              ),
              alignment: Alignment.center,
              child: Text(
                'Tap to dissolve',
                style: TextStyle(
                  color: Theme.of(context).colorScheme.onPrimary,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ),
          ),
        const SizedBox(height: 16),
        TextButton(
          onPressed: () => setState(() => _visible = true),
          child: const Text('Reset'),
        ),
      ],
    );
  }
}

Wrapping the card in NibDissolve with direction: NibDissolveDirection.outward starts it fully visible and dissolves it away over 600ms; onComplete fires once the dissolve finishes, here used to actually remove the widget from the tree (since NibDissolve only paints the dissolve — it doesn’t unmount anything on its own).

direction: which way the dissolve runs

direction is a NibDissolveDirection that picks which end of the dissolve the animation starts from:

NibDissolve(
  direction: NibDissolveDirection.inward,
  duration: const Duration(milliseconds: 800),
  child: const RevealedImage(),
)
ValueBehavior
NibDissolveDirection.inwardStarts fully dissolved (invisible) and animates to fully visible.
NibDissolveDirection.outwardStarts fully visible and animates to fully dissolved (invisible). The default.

Changing direction on a live NibDissolve (e.g. toggling between revealing and hiding the same widget) restarts the dissolve from the new direction’s starting point.

noiseScale: grain size

noiseScale controls the size of the noise grain the dissolve pattern uses — smaller values produce finer, sandier grain; larger values produce coarser, blotchier patches.

NibDissolve(
  noiseScale: 0.05,
  direction: NibDissolveDirection.outward,
  child: const TextureTile(),
)

NibShimmer: a skeleton-loading sweep

NibShimmer paints a diagonal shimmer band sweeping across its child — the loading-skeleton effect most apps build out of an AnimatedContainer and a moving gradient, but driven by a fragment shader instead, so the sweep never triggers a widget rebuild.

import 'package:flutter/material.dart';
import 'package:nib_motion/nib_motion.dart';
 
class SkeletonRow extends StatelessWidget {
  const SkeletonRow({super.key});
 
  @override
  Widget build(BuildContext context) {
    return NibShimmer(
      baseColor: const Color(0xFFE3E3E3),
      shimmerColor: const Color(0xFFF7F7F7),
      child: Row(
        children: [
          Container(
            width: 48,
            height: 48,
            decoration: const BoxDecoration(
              color: Colors.white,
              shape: BoxShape.circle,
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(width: double.infinity, height: 14, color: Colors.white),
                const SizedBox(height: 8),
                Container(width: 140, height: 14, color: Colors.white),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Give the underlying placeholder shapes a plain, light fill (white works well) — NibShimmer composites the sweep over whatever child paints, so the band reads best against a flat, neutral base rather than content with its own strong colors.

baseColor and shimmerColor

baseColor is the background tone the shimmer band is mixed over; shimmerColor is the highlight color of the sweeping band itself. Both default to a neutral light grey pairing when left null, since NibShimmer has no Material dependency to pull a theme color from automatically.

NibShimmer(
  baseColor: Colors.grey.shade300,
  shimmerColor: Colors.grey.shade100,
  child: const PlaceholderBlock(),
)

speed and angle

speed controls how many sweeps per second the band completes — higher values feel more energetic, lower values feel calmer and more deliberate. angle is the sweep angle in radians; the default of -0.3 gives the band a slight diagonal tilt rather than a perfectly horizontal or vertical sweep.

NibShimmer(
  speed: 2.0,
  angle: -0.5,
  child: const PlaceholderBlock(),
)

enabled: pausing the sweep

Set enabled: false to stop the shimmer animation and paint child as-is — useful once real content has loaded and the skeleton is no longer needed, without having to unmount and remount NibShimmer itself.

NibShimmer(
  enabled: isLoading,
  child: isLoading ? const SkeletonBlock() : const RealContent(),
)

API reference

NibDissolve

PropTypeDefaultDescription
childWidgetrequiredThe widget being dissolved in or out.
durationDurationDuration(milliseconds: 500)How long the dissolve animation takes.
directionNibDissolveDirectionNibDissolveDirection.outwardWhether the dissolve animates from invisible to visible (inward) or visible to invisible (outward).
noiseScaledouble0.02The grain size of the dissolve noise pattern. Smaller values produce finer grain.
onCompleteVoidCallback?nullCalled once the dissolve animation finishes.

NibDissolveDirection

ValueDescription
inwardAnimates from fully dissolved to fully visible.
outwardAnimates from fully visible to fully dissolved.

NibShimmer

PropTypeDefaultDescription
childWidgetrequiredThe widget the shimmer sweep is painted over.
baseColorColor?null (resolves to a neutral light grey)The background color the shimmer band is mixed over.
shimmerColorColor?null (resolves to a lighter tint of baseColor)The highlight color of the sweeping band.
speeddouble1.5How fast the shimmer band sweeps across child, in sweeps per second.
angledouble-0.3The sweep angle, in radians.
enabledbooltrueWhether the shimmer animation is active. When false, child is painted with no shimmer overlay.

Next steps

  • AnimateNibDissolve and NibShimmer are standalone widgets, not NibAnim properties, but they compose naturally around any NibMotion subtree that itself animates with initial/animate.
  • Transitions — for the curve- and spring-based timing that drives every other NibMotion animation, complementing the fixed-duration sweeps and dissolves covered here.
  • PresenceNibDissolve pairs naturally with NibPresence’s exit animations: trigger a dissolve outward as a widget’s exit effect so it textures away before NibPresence removes it from the tree.