mirror_skeleton 0.3.1 copy "mirror_skeleton: ^0.3.1" to clipboard
mirror_skeleton: ^0.3.1 copied to clipboard

Render-tree aware shimmer skeletons for Flutter. One-line wrap, zero layout shift, auto-detected widgets, theme-synced colors, and SkeletonIgnore for brand elements.

๐Ÿฆด Mirror Skeleton #

Render-tree aware shimmer skeletons for Flutter. Wrap any widget tree in one line โ€” MirrorSkeleton walks the actual RenderObject tree and generates pixel-matched bones for every text run, image, container, button, form control, and progress indicator it finds. Zero layout shift when your data arrives.

Pub License: MIT Tests Analysis Dart Flutter


โœจ The 30-Second Pitch #

MirrorSkeleton(
  isLoading: loading,
  child: YourPage(),
);

That's it. No parallel placeholder tree to maintain, no per-widget shimmer config. Edit your real UI and the skeleton stays in sync โ€” automatically.



๐ŸŽฌ Demo #

Demo 1 Demo 2 Demo 3
Demo 4 Demo 5 Demo 6
Demo 7 Demo 8


๐ŸŽฏ Why Mirror Skeleton #

Most skeleton libraries make you hand-craft a parallel widget tree that mirrors your real UI, doubling maintenance every time the design shifts. mirror_skeleton inspects the laid-out render tree at paint time and projects a shape for every visible element it finds.

Feature Hand-crafted skeleton mirror_skeleton
Match your real UI manual โœ… automatic
Survives design changes rewrite โœ… free
Zero layout shift only if pixel-perfect โœ… always
Multi-line text wrapping manual line-count math โœ… automatic
Theme-tinted color manual โœ… automatic
Excluded brand elements custom logic โœ… SkeletonIgnore
Buttons / chips / form controls manual placeholders โœ… auto-detected
Hides children from screen readers manual ExcludeSemantics โœ… built in
Blocks taps during loading manual IgnorePointer โœ… built in
Smooth crossfade when loaded manual AnimatedSwitcher โœ… built in
Honors reduced-motion setting manual โœ… built in
Multiple animation styles manual โœ… shimmer / pulse / fade / wave

๐Ÿš€ Quick Start (2 Steps!) #

Step 1: Add to Dependencies #

# pubspec.yaml
dependencies:
  mirror_skeleton: ^0.3.0

Then run:

flutter pub get

Step 2: Wrap Your Widget Tree #

import 'package:mirror_skeleton/mirror_skeleton.dart';

MirrorSkeleton(
  isLoading: _loading,
  child: ProfileBody(user: _user ?? User.placeholder()),
);

That's it! ๐ŸŽ‰ The widget tree under child renders as-is when isLoading is false. While isLoading is true, the same tree is walked at paint time and replaced with shimmering bones matching every visible element โ€” at the exact same positions and sizes.


๐Ÿ“– Complete Usage Guide #

๐Ÿฆด Basic Skeleton Wrap #

class ProfileScreen extends StatefulWidget {
  @override
  State<ProfileScreen> createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  bool _loading = true;
  User? _user;

  @override
  void initState() {
    super.initState();
    _fetchUser();
  }

  Future<void> _fetchUser() async {
    final user = await api.getUser();
    setState(() {
      _user = user;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MirrorSkeleton(
      isLoading: _loading,
      child: ProfileBody(user: _user ?? User.placeholder()),
    );
  }
}

When _loading flips off, the real content fades in over a smooth 250ms crossfade. No visible pop, no jump.

๐Ÿ›ก๏ธ Keep Brand Elements Visible with SkeletonIgnore #

Wrap any subtree in SkeletonIgnore to opt it out of skeletonization:

MirrorSkeleton(
  isLoading: _loading,
  child: Column(
    children: [
      SkeletonIgnore(
        child: Image.asset('assets/logo.png'), // stays visible & tappable
      ),
      ProfileBody(user: user), // shimmers
    ],
  ),
);

The logo stays fully painted while everything around it shimmers. Useful for:

  • Brand logos and hero illustrations
  • Navigation rails / app bars you want to keep functional
  • Background patterns and decorative imagery
  • Any element that should stay visible during loading

๐ŸŽจ Customize the Look #

MirrorSkeleton(
  isLoading: loading,
  shimmerColor: Colors.indigo.shade100,         // optional โ€” bone color
  shimmerDuration: Duration(seconds: 2),        // sweep duration (default 1500ms)
  transitionDuration: Duration(milliseconds: 200), // crossfade out (default 250ms)
  adaptiveSpeed: true,                          // slow shimmer on jank
  shimmerHighlightColor: Colors.white,          // moving highlight color
  shimmerHighlightIntensity: 0.35,              // peak alpha 0.0โ€“1.0
  shimmerDirection: ShimmerDirection.leftToRight,
  style: MirrorSkeletonStyle.shimmer,           // shimmer / pulse / fade / wave
  child: YourPage(),
);

When shimmerColor is omitted, MirrorSkeleton derives a tone from Theme.of(context).colorScheme.primary so the loading state always feels native to your brand โ€” light or dark mode.

๐ŸŒŠ Animation Styles #

Pick the loading vibe that fits your app:

// Default โ€” sweeping highlight
MirrorSkeleton(
  isLoading: loading,
  style: MirrorSkeletonStyle.shimmer,
  child: YourPage(),
);

// Calm in-place opacity oscillation (~55% โ†’ 100%)
MirrorSkeleton(
  isLoading: loading,
  style: MirrorSkeletonStyle.pulse,
  child: YourPage(),
);

// Deeper blink (~20% โ†’ 100%) โ€” more pronounced
MirrorSkeleton(
  isLoading: loading,
  style: MirrorSkeletonStyle.fade,
  child: YourPage(),
);

// Per-bone wave that travels along the shimmer axis
MirrorSkeleton(
  isLoading: loading,
  style: MirrorSkeletonStyle.wave,
  child: YourPage(),
);

โ†”๏ธ Shimmer Direction (4 Options) #

ShimmerDirection.leftToRight   // โžก๏ธ  Default
ShimmerDirection.rightToLeft   // โฌ…๏ธ
ShimmerDirection.topToBottom   // โฌ‡๏ธ
ShimmerDirection.bottomToTop   // โฌ†๏ธ
MirrorSkeleton(
  isLoading: loading,
  shimmerDirection: ShimmerDirection.topToBottom,
  child: YourPage(),
);

๐ŸŽฏ Auto-Detected Widgets #

Anything in this list becomes a properly-shaped bone with no extra code:

๐Ÿ“ Text & Icons #

  • Text, RichText โ€” multi-line text emits one bone per visual line at the actual wrapped width
  • Icon โ€” chunky rounded-square bone matching the icon's footprint

๐Ÿ–ผ๏ธ Images #

  • Image.asset, Image.network, Image.file, Image.memory
  • ClipRRect / ClipOval shapes propagate to descendant images (a ClipRRect(borderRadius: 16) wrapping an image gives a bone with radius 16)

๐Ÿ“ฆ Containers & Surfaces #

  • Container(color: ...), Container(decoration: ...), ColoredBox, DecoratedBox
  • BoxShape.circle becomes a circle bone; rectangular shapes preserve borderRadius
  • Card, Material โ€” emits a low-opacity backdrop for the surface, then layers inner content bones on top (the same pattern production skeletons use for gradient cards, hero artwork, analytics panels, etc.)

๐Ÿ‘ค Avatars #

  • CircleAvatar of any size

๐Ÿ”˜ Buttons & Chips #

  • ElevatedButton, FilledButton, OutlinedButton, TextButton
  • IconButton, FloatingActionButton
  • Chip, ActionChip, InputChip

โ˜‘๏ธ Form Controls (look like the real widget) #

  • TextField, TextFormField โ€” full-field rounded rect
  • Switch โ€” pill track + thumb circle
  • Slider โ€” thin track + thumb
  • Radio โ€” stroked ring
  • Checkbox โ€” outlined rounded square

โณ Progress (intentionally not skeletonised) #

  • A real CircularProgressIndicator, LinearProgressIndicator, or RefreshProgressIndicator is a different kind of loading affordance โ€” stamping a pill on top would be misleading. Their subtree is skipped entirely. Third-party widgets whose render-object class name contains Progress, Loader, or Spinner are also skipped.

๐Ÿ“Š Custom Charts (shape-aware) #

Leaf CustomPaint widgets are detected by aspect:

  • Square small (donut, pie, gauge) โ†’ circle bone
  • Wide (sparkline, bar, line, area) โ†’ row of varying-height bar bones, so the loading state actually signals "chart"
  • Anything else โ†’ rounded-rect fallback

โž– Dividers #

  • Thin border-only decorations render as a hairline bone

๐Ÿ“ Layout Widgets (traversed) #

Row, Column, Stack, Padding, SizedBox, AspectRatio, Expanded, Flexible, ListView, GridView, SingleChildScrollView, CustomScrollView โ€” traversed and used to compute exact bone positions via RenderBox.localToGlobal.


๐Ÿ› ๏ธ Built-in Polish #

Things you don't have to remember to wire up:

  • ๐Ÿšซ Hit testing absorbed during loading. Taps don't leak through to underlying widgets โ€” onPressed handlers won't fire while the skeleton is up.
  • ๐Ÿ”‡ Semantics excluded during loading. Screen readers see a single Loading live region instead of reading placeholder text.
  • ๐ŸŽฌ Smooth crossfade when isLoading flips off. Real content fades in over transitionDuration (250ms by default) โ€” no visible pop.
  • โ™ฟ Reduced motion respected. When MediaQuery.of(context).disableAnimations is true, the shimmer stops sweeping and the bones stay static.
  • โšก Adaptive shimmer speed. Frame timings are sampled and the shimmer slows automatically on devices that drop frames.
  • ๐Ÿ’ค TickerMode-aware. A MirrorSkeleton inside an off-screen TabBarView page (or any TickerMode(enabled: false) subtree) stops its shimmer ticker so it doesn't burn frames when invisible.
  • ๐Ÿ“ Zero layout shift. Bones are derived from the laid-out subtree, so the real content slots into exactly the same coordinates.
  • ๐Ÿงน Memory released after fade-out. When the crossfade completes, the bone list, ignored-region list, and descendant cache are cleared so the render object stops pinning descendants in memory.

๐Ÿ’ก Real-World Examples #

Profile Page with Avatar, Bio & Stats #

class ProfilePage extends StatefulWidget {
  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  bool _loading = true;
  User? _user;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    await Future.delayed(Duration(seconds: 2));
    setState(() {
      _user = await api.getUser();
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: MirrorSkeleton(
        isLoading: _loading,
        child: Column(
          children: [
            CircleAvatar(radius: 50, backgroundImage: NetworkImage(user.avatar)),
            Text(user.name, style: TextStyle(fontSize: 24)),
            Text(user.bio, maxLines: 3),
            Row(children: [
              StatChip(label: 'Followers', value: user.followers),
              StatChip(label: 'Following', value: user.following),
              StatChip(label: 'Posts', value: user.posts),
            ]),
          ],
        ),
      ),
    );
  }
}

Feed List with Hero Image Excluded #

MirrorSkeleton(
  isLoading: _loading,
  child: CustomScrollView(
    slivers: [
      SliverToBoxAdapter(
        // Brand hero stays visible the whole time
        child: SkeletonIgnore(
          child: Image.asset('assets/hero.png'),
        ),
      ),
      SliverList.builder(
        itemBuilder: (_, i) => ArticleTile(article: articles[i]),
        itemCount: articles.length,
      ),
    ],
  ),
);

Tab View with Per-Tab Loading #

TabBarView(
  children: [
    MirrorSkeleton(isLoading: _loadingFeed,    child: FeedPage()),
    MirrorSkeleton(isLoading: _loadingTrending, child: TrendingPage()),
    MirrorSkeleton(isLoading: _loadingSaved,   child: SavedPage()),
  ],
);

Each tab shimmers independently, and off-screen tabs pause their shimmer ticker automatically (TickerMode-aware) โ€” no wasted frames.

Pull-to-Refresh #

RefreshIndicator(
  onRefresh: _load,
  child: MirrorSkeleton(
    isLoading: _loading,
    child: ListView.builder(
      itemBuilder: (_, i) => ArticleTile(article: articles[i]),
      itemCount: articles.length,
    ),
  ),
);

The RefreshProgressIndicator is intentionally skipped during skeletonization, so the spinner remains visible above the shimmering list โ€” exactly like production apps.


๐Ÿ“š API Reference #

MirrorSkeleton #

Parameter Type Default Notes
isLoading bool required Show skeleton vs. real child
child Widget? โ€“ The tree to skeletonize
shimmerColor Color? derived from theme Bone color. null โ†’ derived from Theme.of(context).colorScheme.primary
shimmerDuration Duration 1500ms One full sweep
transitionDuration Duration 250ms Crossfade duration. Duration.zero to disable
adaptiveSpeed bool true Slow shimmer on frame drops
shimmerHighlightColor Color Colors.white Color of the moving highlight
shimmerHighlightIntensity double 0.35 Peak alpha of the highlight, 0.0โ€“1.0
shimmerDirection ShimmerDirection leftToRight Sweep direction
style MirrorSkeletonStyle shimmer shimmer / pulse / fade / wave

SkeletonIgnore #

Wraps a subtree and renders it normally over the skeleton. Useful for brand logos, hero illustrations, or anything that should remain visible during loading.

SkeletonIgnore(child: YourBrandWidget())

ShimmerDirection #

enum ShimmerDirection { leftToRight, rightToLeft, topToBottom, bottomToTop }

MirrorSkeletonStyle #

enum MirrorSkeletonStyle { shimmer, pulse, fade, wave }
Style Effect
shimmer Sweeping highlight gradient โ€” the classic skeleton look
pulse Calm in-place opacity oscillation (~55% โ†” 100%)
fade Pronounced blink (~20% โ†” 100%)
wave Per-bone alpha wave traveling along the shimmer axis โ€” gentler than shimmer, shader-free

๐Ÿง  Best Practices #

โœ… Do's #

  • โœ“ Wrap the smallest meaningful subtree (the body of a screen, a list, a card) โ€” not the whole MaterialApp
  • โœ“ Render real widgets with placeholder data while loading โ€” User.placeholder(), List.filled(6, Article.placeholder()) โ€” so layout matches the loaded state
  • โœ“ Use SkeletonIgnore for brand logos, hero images, navigation chrome
  • โœ“ Trust the auto-detection โ€” it covers virtually all standard Flutter widgets
  • โœ“ Test with dark mode โ€” theme-derived colors adapt automatically

โŒ Don'ts #

  • โœ— Don't toggle isLoading rapidly โ€” let the crossfade finish for a polished feel
  • โœ— Don't wrap a Scaffold directly โ€” wrap the body, not the whole Scaffold (so the AppBar stays interactive)
  • โœ— Don't worry about progress indicators inside the tree โ€” they're auto-skipped, not skeletonized
  • โœ— Don't forget to provide placeholder data to your widgets while loading; MirrorSkeleton shapes bones from what's actually laid out

๐Ÿ†˜ Troubleshooting #

Issue Solution
Skeleton shows nothing Make sure isLoading: true and that the child actually lays out (provide placeholder data so widgets get a real size)
Real content "pops" in Increase transitionDuration for a softer crossfade, or set it to Duration.zero if you want a hard cut
Shimmer feels laggy adaptiveSpeed: true is on by default โ€” try release mode (flutter run --release) for true performance
Bones at wrong positions Wrap a smaller subtree closer to the actual content, not the whole app
Logo gets shimmered Wrap it in SkeletonIgnore
Custom widget not detected Wrap it in a sized Container(color: ...) during loading, or use SkeletonIgnore to keep it visible
Rotated bone has no shimmer overlay Known: Transform.rotate / RotatedBox bones are drawn at the correct rotated position but the shimmer highlight skips them โ€” they still receive the bone color

โš ๏ธ Limitations #

  • Transform.rotate / RotatedBox bones are drawn at the correct rotated position, but the moving shimmer highlight skips them โ€” they still receive the bone color, just without the sweep overlay.
  • Truly bespoke RenderBox subclasses outside the patterns above: fall back to wrapping in a Container(color: ...) of equivalent size during loading, or wrap in SkeletonIgnore to keep them visible.

๐Ÿ“Š Quality & Testing #

  • โœ… 30/30 tests passing (100%)
  • โœ… 0 code analysis issues
  • โœ… Full null safety
  • โœ… Dartdoc on all public API
  • โœ… Flutter 3.27.0+
  • โœ… Dart 3.11.3+

๐Ÿ“ Project Structure #

mirror_skeleton/
โ”œโ”€โ”€ lib/
โ”‚   โ”œโ”€โ”€ mirror_skeleton.dart          # Public entrypoint
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ render_mirror_skeleton.dart  # Render object & owner element
โ”‚       โ”œโ”€โ”€ bone_detection.dart       # Render-tree โ†’ bones
โ”‚       โ”œโ”€โ”€ bone_painting.dart        # Bones โ†’ canvas
โ”‚       โ””โ”€โ”€ bones.dart                # Bone, BoneType, ShimmerDirection, MirrorSkeletonStyle
โ”œโ”€โ”€ test/
โ”‚   โ””โ”€โ”€ mirror_skeleton_test.dart     # 30 widget tests
โ”œโ”€โ”€ example/                          # 14-page demo gallery
โ”‚   โ””โ”€โ”€ lib/
โ”‚       โ””โ”€โ”€ pages/                    # Profile, Feed, Grid, Article, Chat, Dashboard, Controls, โ€ฆ
โ””โ”€โ”€ pubspec.yaml

๐ŸŽฌ Example App #

A complete demo gallery is in example/ covering:

  • Profile โ€” Avatar, multi-line bio, stat row
  • Feed โ€” ListView of article tiles with images
  • Product Grid โ€” GridView with cards, prices, ratings
  • Article โ€” Hero image, paragraphs, author row
  • Messages / Conversations โ€” Chat list with avatars and unread badges
  • Dashboard โ€” Mixed layout with SkeletonIgnore brand banner
  • Controls Gallery โ€” Buttons, chips, switches, sliders, progress, text fields
  • Login โ€” TextFields, password toggle, Checkbox, social buttons
  • Settings โ€” Sectioned ListTiles with Switch, Checkbox, Slider, Divider
  • Music Player โ€” Hero artwork, sliders, transport row, queue list
  • Wallet โ€” Gradient balance card, action chips, transaction list
  • Analytics โ€” Sparkline + bar + donut CustomPaint charts and stats
  • Shimmer Styles โ€” Live playground for direction, style, color, intensity

Run it:

cd example
flutter pub get
flutter run

๐Ÿค Contributing #

Contributions are welcome! Please:

  1. Report issues on GitHub
  2. Submit pull requests with improvements
  3. Help improve documentation

๐Ÿ“„ License #

MIT License โ€” see LICENSE for details.


๐Ÿ“š Resources #


Made with ๐Ÿฆด for the Flutter community

โญ Star on GitHub if mirror_skeleton saves you a Sunday afternoon!

7
likes
155
points
227
downloads

Documentation

API reference

Publisher

verified publishersakarc.com.np

Weekly Downloads

Render-tree aware shimmer skeletons for Flutter. One-line wrap, zero layout shift, auto-detected widgets, theme-synced colors, and SkeletonIgnore for brand elements.

Homepage
Repository (GitHub)
View/report issues

Topics

#skeleton #shimmer #loading #placeholder #ui

License

MIT (license)

Dependencies

flutter

More

Packages that depend on mirror_skeleton