Skip to content

Source on GitHublib/src/ui_kit/fast_shimmer/

Overview

The FastShimmer family provides hand-crafted loading skeletons with a synchronized highlight sweep. Compose layouts with FastShimmerBox / FastShimmerCircle / FastShimmerText / FastShimmerList, and let FastShimmerScope drive one shared AnimationController plus ShaderMask so every descendant stays in phase.

TopicNotes
Entry pointFastShimmer(isLoading, skeleton, child)
Sync animationFastShimmerScope (auto-wrapped by FastShimmer when no ancestor scope exists)
PlaceholdersBox / Circle / Text / List
ThemingFastShimmerTheme (ThemeExtension) + FastShimmerDirection
Not providedAuto-inferring skeleton shapes from child

TIP

Skeleton children must be opaque (package placeholders default to white) so the ShaderMask gradient is visible. See the example app’s ShimmerExample page for a full demo.


Examples

Loading switch (recommended entry):

dart
import 'package:flutter/material.dart';
import 'package:fast_package/fast_package.dart';

FastShimmer(
  isLoading: _loading,
  skeleton: const Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      FastShimmerBox(width: double.infinity, height: 180),
      SizedBox(height: 12),
      Row(
        children: [
          FastShimmerCircle(diameter: 48),
          SizedBox(width: 12),
          FastShimmerText(lines: 2, width: 160),
        ],
      ),
    ],
  ),
  child: MyRealContent(),
);

Skeleton only, with an explicit scope:

dart
FastShimmerScope(
  child: Column(
    children: [
      FastShimmerBox(width: double.infinity, height: 180),
      SizedBox(height: 12),
      FastShimmerCircle(diameter: 48),
      FastShimmerText(lines: 2, width: 160),
    ],
  ),
);

Theme setup

Register on ThemeData.extensions for app-wide base/highlight colors and direction (cycle length can still be overridden via Scope / FastShimmer.duration):

dart
MaterialApp(
  theme: ThemeData(
    brightness: Brightness.light,
    extensions: const [FastShimmerTheme.light],
  ),
  darkTheme: ThemeData(
    brightness: Brightness.dark,
    extensions: const [FastShimmerTheme.dark],
  ),
  // ...
);

Resolution order:

  1. Explicit widget overrides (e.g. standalone baseColor)
  2. FastShimmerTheme on ThemeData
  3. Fallback to FastShimmerTheme.light / dark from ThemeData.brightness

API reference


FastShimmer

dart
const FastShimmer({
  required Widget child,
  required bool isLoading,
  required Widget skeleton,
  Duration duration = const Duration(milliseconds: 1500),
});
ParameterTypeRequiredDescription
childWidgetyesReal content when isLoading is false
isLoadingboolyestrueskeleton; falsechild
skeletonWidgetyesHand-built skeleton; not inferred from child
durationDurationnoCycle length when auto-creating a Scope; ignored if an ancestor Scope exists

While loading, Semantics use label Loading with excludeSemantics. If an ancestor FastShimmerScope already exists, a second scope is not wrapped (avoids double ShaderMask).


FastShimmerScope

dart
const FastShimmerScope({
  required Widget child,
  Duration duration = const Duration(milliseconds: 1500),
});
ParameterTypeRequiredDescription
childWidgetyesSkeleton subtree under the shimmer mask
durationDurationnoOne full highlight cycle; default 1500 ms

Static helpers:

MethodDescription
of(context)Nearest scope animation value 0.01.0; 0.5 if none (subscribes)
maybeOf(context)Animation value or null (subscribes when found)
hasScope(context)Whether an ancestor Scope exists without per-frame subscription

When MediaQuery.disableAnimations is true, the controller stops and freezes at 0.5. AnimatedBuilder caches child, so placeholders are not rebuilt every tick.


FastShimmerTheme

dart
const FastShimmerTheme({
  required Color baseColor,
  required Color highlightColor,
  required Duration duration,
  required FastShimmerDirection direction,
});
MemberDescription
baseColorBackground color
highlightColorTraveling highlight
durationTheme cycle (align with Scope duration for a consistent feel)
directionHighlight travel direction
light / darkBuilt-in defaults
of(context)Extension lookup; may be null
resolve(context, {…})Effective theme with optional field overrides
copyWith / lerpStandard ThemeExtension behavior; direction switches at t == 0.5

Defaults (summary):

ThemebaseColorhighlightColordurationdirection
light#E0E0E0#F5F5F51500 msleftToRight
dark#2C2C2C#3D3D3D1500 msleftToRight

FastShimmerDirection

ValueHighlight travel
leftToRightLeft → right
rightToLeftRight → left
topToBottomTop → bottom
bottomToTopBottom → top
diagonalTop-left → bottom-right

toGradient({colors, stops}) builds a LinearGradient for that axis (Scope shifts stops each frame).


FastShimmerBox

dart
const FastShimmerBox({
  required double width,
  required double height,
  BorderRadius borderRadius = BorderRadius.zero,
  Color? baseColor,
  Color? highlightColor,
  FastShimmerDirection? direction,
});
ParameterDescription
width / heightRectangle size (logical pixels)
borderRadiusCorners; sharp by default
baseColorOverrides base color in standalone mode only
highlightColor / directionReserved for API consistency; unused inside a Scope

Inside a Scope the fill is white; without a Scope it uses baseColor or the theme base (static).


FastShimmerCircle

dart
const FastShimmerCircle({
  required double diameter,
  Color? baseColor,
  Color? highlightColor,
  FastShimmerDirection? direction,
});
ParameterDescription
diameterCircle diameter (logical pixels)
baseColor, etc.Same standalone / Scope fill rules as FastShimmerBox

FastShimmerText

dart
const FastShimmerText({
  int lines = 3,
  double lineHeight = 12.0,
  double lineSpacing = 6.0,
  double lastLineWidthFraction = 0.6,
  double width = 200.0,
  Color? baseColor,
  Color? highlightColor,
  FastShimmerDirection? direction,
});
ParameterDescription
linesNumber of bars; at least 1
lineHeight / lineSpacingBar height and gap
widthFull width of non-last lines
lastLineWidthFractionLast line as a fraction of width, range (0, 1]

FastShimmerList

dart
const FastShimmerList({
  required int itemCount,
  Widget Function(int index)? itemBuilder,
  double separatorHeight = 12.0,
  EdgeInsetsGeometry padding = EdgeInsets.zero,
  bool shrinkWrap = true,
  ScrollPhysics? physics = const NeverScrollableScrollPhysics(),
});
ParameterDescription
itemCountRow count; non-negative
itemBuilderCustom row; default is avatar circle + two text lines
separatorHeightGap between rows
padding / shrinkWrap / physicsList layout; non-scrollable by default for nesting

Wrap with FastShimmerScope (or FastShimmer) so rows share one highlight.


Recipes

List skeleton

dart
FastShimmer(
  isLoading: _loading,
  skeleton: FastShimmerList(
    itemCount: 5,
    itemBuilder: (index) => const Padding(
      padding: EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          FastShimmerCircle(diameter: 40),
          SizedBox(width: 12),
          FastShimmerText(lines: 2, width: 180),
        ],
      ),
    ),
  ),
  child: RealFeedList(),
);

Local direction / color override

Inject FastShimmerTheme with an inner Theme for a preview region:

dart
Theme(
  data: Theme.of(context).copyWith(
    extensions: [
      FastShimmerTheme.light.copyWith(
        direction: FastShimmerDirection.diagonal,
        highlightColor: const Color(0xFFFFFFFF),
      ),
    ],
  ),
  child: FastShimmerScope(
    child: FastShimmerBox(width: 200, height: 24),
  ),
);

Standalone static placeholders

Without a Scope, placeholders still render (theme base color) but without motion—useful for layout debugging or when animation is not needed yet.


Notes

  • Hand-crafted only: skeleton is required; there is no auto-detect / shape detector.
  • One Scope: Prefer one scope per screen or loading subtree; nesting is allowed but usually unnecessary.
  • Accessibility: Honors MediaQuery.disableAnimations; loading is announced as Loading by FastShimmer.
  • Performance: The sweep runs at the Scope layer; placeholders are not rebuilt every animation frame.

Released under the MIT License