Watch the Coding Liquids comparison to see how closely matched skeleton geometry prevents a layout shift.
Use skeletons when layout is predictable
A skeleton preserves the eventual page structure and communicates that specific content is loading. It reduces layout shift and can feel faster than a spinner on card-heavy feeds.
For an unknown or very short operation, a progress indicator may communicate more honestly.
flutter pub add shimmer
Apply Shimmer.fromColors to placeholders
The shimmer package moves a highlight between baseColor and highlightColor across child shapes. Build neutral Containers beneath Shimmer.fromColors and avoid placing real readable content inside the effect.
Low contrast makes movement invisible, while extreme contrast creates a distracting flash.
dependencies:
flutter:
sdk: flutter
shimmer: any
Instagram before-and-after frames compare skeleton geometry with the content that replaces it.
Match the actual card geometry
Copy avatar diameter, image aspect ratio, line widths, padding, and corner radii from the loaded card. Vary placeholder text widths slightly to suggest hierarchy without inventing content.
A generic stack of grey bars still causes a jump when the real layout appears.

Build production-ready Shimmer Loading Effect Tutorial features
The Complete Flutter Guide turns shimmer loading effect tutorial into maintainable app architecture, polished UI, and testable production code.
Enrol nowImport shimmer for Shimmer.fromColors; the later ShaderMask version needs no third-party effect widget.
import 'package:shimmer/shimmer.dart';
Swap skeletons for content once
Render the skeleton while the Future is waiting, the content after data arrives, and a real error state after failure. AnimatedSwitcher can soften the replacement when its children have different keys.
Leaving shimmer active behind loaded content continues unnecessary animation work.
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class ArticleSkeleton extends StatelessWidget {
const ArticleSkeleton({super.key});
@override
Widget build(BuildContext context) => Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Column(children: List.generate(4, (_) => Container(
height: 72,
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
))),
);
}
Build a ShaderMask shimmer yourself
Animate a LinearGradient transform with AnimationController and apply it through ShaderMask. Use TileMode.clamp, a narrow highlight stop, and a RepaintBoundary around repeated skeletons.
A custom controller must be disposed and should stop when its route is not visible.
Widget articleBody(AsyncSnapshot<List<Article>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const ArticleSkeletonList();
}
if (snapshot.hasError) return const ArticleLoadError();
return ArticleList(articles: snapshot.data ?? const []);
}
The LinkedIn loading-state discussion separates perceived progress, accessibility, and repaint cost.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowPrefer a spinner for indeterminate actions
A button submit, one small inline lookup, or an operation with no known result layout often needs only CircularProgressIndicator. Choose semanticsLabel and a compact size appropriate to the control.
A full-page skeleton for a single button action misrepresents what is loading.
class MovingShimmer extends AnimatedWidget {
const MovingShimmer({super.key, required Animation<double> animation, required this.child})
: super(listenable: animation);
final Widget child;
@override
Widget build(BuildContext context) {
final value = (listenable as Animation<double>).value;
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) => LinearGradient(
begin: Alignment(-1.5 + value * 3, 0),
end: Alignment(-0.5 + value * 3, 0),
colors: const [Color(0xFFE5E7EB), Color(0xFFF8FAFC), Color(0xFFE5E7EB)],
).createShader(bounds),
child: child,
);
}
}
Make the placeholder match the eventual layout
A skeleton is useful when it preserves the geometry of content that is likely to arrive soon. Match the real card’s width, height, padding, border radius, avatar position, and line lengths closely enough that replacement does not shift neighbouring controls. Use a different empty state when the request has completed with no records; continuing to shimmer falsely suggests that data is still on its way.
The highlight can be produced by moving a gradient through a shader while the placeholder shapes remain static. Keep one animation owner around a group of related skeletons instead of giving every rectangle a controller. Repainting a full scrolling screen on each tick is expensive, so constrain the animated region and inspect repaint boundaries with profiling tools. Subtle contrast and a moderate sweep communicate activity without creating a distracting flashing surface.
Loading semantics need separate treatment from the painted blocks. Exclude meaningless placeholder rectangles from accessibility traversal, expose one concise loading announcement for the region, and restore focus predictably when content replaces it. If a refresh leaves usable rows on screen, an inline progress indicator may be better than replacing everything with skeletons. Reduced-motion preferences should produce static placeholders or a conventional progress indicator rather than an endless moving highlight.
Test slow success, immediate cached success, empty success, failure, retry, cancellation, and a response that arrives after the route is disposed. Golden tests can compare skeleton and loaded geometry at several device widths and text scales. Profile a release build with the maximum number of visible placeholders and confirm that off-screen animations stop. The final state must be driven by request data, not by a fixed timer; otherwise a fast response waits unnecessarily and a slow response exposes blank content when the cosmetic delay expires.
Shared shimmer direction should follow the layout’s reading direction when it conveys a travelling highlight. Resolve left-to-right and right-to-left behaviour explicitly, then inspect rounded corners and clipped images for shader discontinuities. Placeholder colour also belongs to the theme: light and dark surfaces need separate low-contrast pairs that remain perceivable without resembling enabled controls. If the application supports data-saving or battery-conscious modes, stopping ornamental shimmer is an easy win. The content repository, not the shimmer widget, should own timeout and retry decisions so changing the loading visual cannot alter network correctness.
Nested scroll views can multiply shimmer work when each section owns an independent ticker. A page-level loading coordinator may share timing while each skeleton keeps its own geometry. Stop the ticker as soon as no shimmer region is visible, and confirm that a tab kept alive off-screen does not continue repainting. These savings matter most on the slow devices for which loading feedback remains visible longest.
Loading belongs to the repository state machine, while shimmer is only one rendering of its initial pending state. Cached rows with a refresh in progress should normally remain visible with a smaller status indicator. A first load can show skeletons, an empty success needs an empty-state message, and a failure needs explanation plus retry. Modelling those cases prevents a permanent shimmer when a future completes with no data or an exception is accidentally swallowed.
Responsive skeletons should use the same breakpoints and content constraints as their loaded counterparts. Compare card height, avatar diameter, line count, and grid columns at phone, tablet, and large text sizes. Give the group one semantics label and exclude each decorative bar, then announce the loaded heading when replacement occurs without repeatedly interrupting a screen reader. Track the duration for which skeletons remain visible as a performance signal, but do not add a cosmetic minimum delay. Fast cached content should appear immediately, and slow content should remain honest about its state.
Common mistakes
- Use skeletons when layout is predictable: In shimmer skeletons, for an unknown or very short operation, a progress indicator may communicate more honestly; inspect this shimmer skeletons cause before changing another shimmer skeletons widget.
- Apply Shimmer.fromColors to placeholders: In shimmer skeletons, low contrast makes movement invisible, while extreme contrast creates a distracting flash; inspect this shimmer skeletons cause before changing another shimmer skeletons widget.
- Match the actual card geometry: In shimmer skeletons, a generic stack of grey bars still causes a jump when the real layout appears; inspect this shimmer skeletons cause before changing another shimmer skeletons widget.
- Swap skeletons for content once: In shimmer skeletons, leaving shimmer active behind loaded content continues unnecessary animation work; inspect this shimmer skeletons cause before changing another shimmer skeletons widget.
- Build a ShaderMask shimmer yourself: In shimmer skeletons, a custom controller must be disposed and should stop when its route is not visible; inspect this shimmer skeletons cause before changing another shimmer skeletons widget.
- Prefer a spinner for indeterminate actions: In shimmer skeletons, a full-page skeleton for a single button action misrepresents what is loading; inspect this shimmer skeletons cause before changing another shimmer skeletons widget.
Frequently asked questions
How does use skeletons when layout is predictable work in shimmer skeletons?
For shimmer skeletons, the starting rule is that a skeleton preserves the eventual page structure and communicates that specific content is loading. Apply this shimmer skeletons rule first because use skeletons when layout is predictable determines whether the shimmer skeletons pattern fits.
Why does match the actual card geometry matter for shimmer skeletons?
In shimmer skeletons, vary placeholder text widths slightly to suggest hierarchy without inventing content. Keeping match the actual card geometry at the shimmer skeletons call site exposes the shimmer skeletons return value directly.
What failure should I test first in shimmer skeletons?
First reproduce the shimmer skeletons case where a custom controller must be disposed and should stop when its route is not visible. The corrected shimmer owns and disposes one controller, pauses off-screen, and yields immediately to success, empty, or error content.
How can I verify shimmer skeletons before release?
Exercise prefer a spinner for indeterminate actions with real shimmer skeletons inputs on every shipped platform. Inspect the final shimmer skeletons callback or output; a successful shimmer skeletons button tap alone is not proof.
Further reads
Connect shimmer skeletons to the surrounding Flutter stack through these published tutorials:
- Flutter Development Guide 2026
- Flutter AnimatedContainer: Animate Without Controllers
- Flutter FutureBuilder vs StreamBuilder: Render Async Data Safely
- Flutter ListView.builder: Efficient, Dynamic Scrolling Lists
- Flutter AnimationController and Tween Explained
Sources: Flutter documentation and the package documentation on pub.dev; shimmer skeletons examples verified against current stable Flutter.