auto_skeleton

pub package License: BSD-3

Skeletons that stay in sync with your UI — edit once, not twice.

Wrap any widget with AutoSkeleton and it introspects the tree to generate matching bones automatically. Redesign the real widget and the skeleton follows — no parallel placeholder layout to maintain.

  • Data-independent UIs (profile cards, detail screens, forms): zero boilerplate — just wrap
  • Lists / grids: pass one skeletonItem template and it repeats N times (lighter than maintaining a full mock data model)
  • Theme-aware, async-aware, accessibility-aware out of the box

Screenshots

Skeleton (Loading) Loaded (Content)

Annotations: Fine-grained Control

Annotation Skeleton Annotation Loaded
  • PlaceholderIgnore — the toggle switches are hidden during loading (not relevant to skeleton)
  • PlaceholderLeaf — the colored icon boxes are treated as solid rectangles (no child traversal)

Why auto_skeleton?

The real cost of skeleton loading states isn't the first-time build — it's the drift. Every time you redesign a screen, the manually-crafted placeholder in shimmer goes out of date. With skeletonizer, you update the skeleton by updating mock data (which you maintain separately from real widgets).

auto_skeleton keeps both in one place: your real widget tree. Change the widget, the skeleton follows — automatic.

When to pick what

Your situation Best tool
Data-independent layout (profile card, form, detail screen) auto_skeleton — zero boilerplate, just wrap
List/grid where data changes the layout auto_skeleton with skeletonItem template (or skeletonizer with mock data — both work)
Pixel-exact designer-spec placeholders shimmer — manual control
Custom-painted, markdown, SVG, chart-heavy UI shimmer — less surface area for surprises
Simple one-off animation on known shapes shimmer — smallest footprint

Feature comparison

Feature auto_skeleton skeletonizer shimmer
Auto-match widget shapes Yes Yes No
Layout-driven skeleton (no mock data model) Yes No (needs mock objects) No
List template via one widget Yes (skeletonItem) Via mock data list No
Async builder (no setState) Yes No No
Future + Stream support Yes No No
Theme-aware colors Yes Yes No
Reduce-motion accessibility Yes (auto) Yes No
Debug overlay (debugShowBones) Yes No No
Extension syntax .withSkeleton() Yes No No
Pre-built presets Yes No No
Multiple effects (shimmer / pulse / solid) Yes Yes Shimmer only
Annotation system (Ignore, Leaf, Replace) Yes Yes No
Switch animation Yes Yes No
Per-state bone caching Yes N/A No
App-level wrapper Yes No No

Comparison based on default features of each package as of April 2026. All three packages are actively maintained and excellent in their own right.

Installation

dependencies:
  auto_skeleton: ^0.4.1
flutter pub get

Quick Start

Basic Usage

Wrap any widget with AutoSkeleton:

AutoSkeleton(
  enabled: _isLoading,
  child: Card(
    child: ListTile(
      leading: CircleAvatar(child: Icon(Icons.person)),
      title: Text('John Doe'),
      subtitle: Text('Software Developer'),
      trailing: Icon(Icons.chevron_right),
    ),
  ),
)

That's it! When enabled: true, the package scans the widget tree and renders matching skeleton bones with a shimmer animation. When enabled: false, your actual content is shown.

Colors are automatically derived from your app's theme — works in both light and dark mode with zero configuration.

List Skeleton — No Fake Data, No Mock Items

The most common skeleton problem: your list is empty while loading, so there's nothing to scan. Use skeletonItem + skeletonItemCount to provide one template widget — the package repeats it N times and scans that instead:

AutoSkeleton(
  enabled: _isLoading,
  skeletonItem: ListTile(
    leading: CircleAvatar(child: Icon(Icons.person)),
    title: Text('Name'),
    subtitle: Text('Description'),
  ),
  skeletonItemCount: 6,        // show 6 placeholder rows
  child: ListView.builder(     // real list — empty while loading is fine
    itemCount: items.length,
    itemBuilder: (_, i) => MyListTile(item: items[i]),
  ),
)

No fake data needed. The template is one real-looking widget with any text — the package replaces it with bones automatically.

skeletonizer auto_skeleton
List loading — fake data required Yes, N items No — one template
Works when list is empty No Yes
Template repeated N times No Yes

Debugging: "my skeleton is blank"

If you wrap something and see a blank/empty skeleton, the scanner didn't find any introspectable leaves (Text, Icon, Image, CircleAvatar, buttons, etc). Two tools:

AutoSkeleton(
  enabled: true,
  debugShowBones: true,   // ← overlay red outlines on every detected bone
  skeletonItem: MyTemplate(),
  child: realList,
)

In debug mode, if the scan produces 0 bones, AutoSkeleton prints a warning in the console telling you exactly that. Common cause: your template is made of Containers and SizedBoxes with no leaves — wrap custom widgets with PlaceholderLeaf or add real Text/Icon inside.

AutoSkeletonBuilder — Zero setState

Handle async data loading with automatic skeleton. No setState, no _isLoading boolean:

AutoSkeletonBuilder<User>(
  future: fetchUser(),
  skeleton: ListTile(
    leading: CircleAvatar(child: Icon(Icons.person)),
    title: Text('Placeholder name'),
    subtitle: Text('Loading...'),
  ),
  builder: (context, user) => ListTile(
    leading: CircleAvatar(backgroundImage: NetworkImage(user.avatar)),
    title: Text(user.name),
    subtitle: Text(user.bio),
  ),
)

Works with Stream too:

AutoSkeletonBuilder<List<Post>>(
  stream: postStream(),
  skeleton: MyPostListSkeleton(),
  builder: (context, posts) => PostList(posts),
  errorBuilder: (context, error) => ErrorWidget(error),
)

Extension Syntax

Even simpler — use the .withSkeleton() extension:

Card(
  child: ListTile(
    title: Text('Hello World'),
    subtitle: Text('This is a subtitle'),
  ),
).withSkeleton(loading: _isLoading)

Migrating from Other Packages

From shimmer

// Before (shimmer) — manual layout, no auto-detection
Shimmer.fromColors(
  baseColor: Colors.grey[300]!,
  highlightColor: Colors.grey[100]!,
  child: Column(
    children: [
      Container(width: 48, height: 48, color: Colors.white),
      Container(width: 200, height: 16, color: Colors.white),
      Container(width: 150, height: 14, color: Colors.white),
    ],
  ),
)

// After (auto_skeleton) — one line, auto-detected
AutoSkeleton(
  enabled: _isLoading,
  child: myActualWidget,  // your real widget, real data
)

From skeletonizer

// Before (skeletonizer) — requires fake/mock data
Skeletonizer(
  enabled: _isLoading,
  child: ListTile(
    title: Text('Fake Name Here'),        // fake data!
    subtitle: Text('Fake email@test.com'), // fake data!
    leading: CircleAvatar(
      backgroundImage: NetworkImage('https://fake-url.com/img'), // fake!
    ),
  ),
)

// After (auto_skeleton) — zero fake data
AutoSkeleton(
  enabled: _isLoading,
  child: myActualWidget,  // same widget, real data, no mocks
)

Key differences when migrating:

  • No fake data needed — use your actual widgets
  • Colors are theme-aware by default (remove manual color setup)
  • Use .withSkeleton(loading: true) for even cleaner syntax
  • Use AutoSkeletonBuilder to eliminate setState + _isLoading boilerplate entirely

Color Customization

Layer 1: Theme-aware (zero config)

Colors are automatically derived from your app's ColorScheme. Just works in light and dark mode.

Layer 2: Global override

Set colors once at the app root:

AutoSkeletonConfig(
  data: AutoSkeletonConfigData(
    baseColor: Colors.grey.shade300,
    highlightColor: Colors.grey.shade100,
  ),
  child: MaterialApp(...),
)

Layer 3: Per-widget override

Override on a specific widget:

AutoSkeleton(
  enabled: _isLoading,
  effect: ShimmerEffect(baseColor: Colors.blue.shade200),
  child: myWidget,
)

Effects

Shimmer (Default)

AutoSkeleton(
  enabled: _isLoading,
  effect: ShimmerEffect(
    baseColor: Colors.grey.shade300,
    highlightColor: Colors.grey.shade100,
    duration: Duration(milliseconds: 1500),
    direction: ShimmerDirection.ltr,
  ),
  child: MyWidget(),
)

Pulse

A gentle breathing/fade animation:

AutoSkeleton(
  enabled: _isLoading,
  effect: PulseEffect(
    color: Colors.blue.shade100,
    duration: Duration(milliseconds: 1200),
  ),
  child: MyWidget(),
)

Solid

Static placeholder with no animation:

AutoSkeleton(
  enabled: _isLoading,
  effect: SolidEffect(color: Colors.grey.shade200),
  child: MyWidget(),
)

Annotations

Control how specific widgets are skeletonized:

PlaceholderIgnore

Hide a widget completely during loading — useful for interactive elements (switches, buttons) that don't make sense in a skeleton:

AutoSkeleton(
  enabled: _isLoading,
  child: ListTile(
    title: Text('Notifications'),
    subtitle: Text('Push & email alerts'),
    trailing: PlaceholderIgnore(
      child: Switch(value: true, onChanged: (_) {}),
    ),
  ),
)

PlaceholderLeaf

Mark complex widgets (charts, maps, custom painters) as a single solid bone instead of traversing their children:

PlaceholderLeaf(
  borderRadius: BorderRadius.circular(12),
  child: MyComplexChartWidget(),
)

PlaceholderReplace

Replace a widget with a completely custom placeholder:

PlaceholderReplace(
  replacement: Container(
    width: 48, height: 48,
    decoration: BoxDecoration(
      color: Colors.grey.shade300,
      shape: BoxShape.circle,
    ),
  ),
  child: CircleAvatar(
    backgroundImage: NetworkImage(user.avatarUrl),
  ),
)

Pre-built Presets

Ready-to-use skeleton patterns for common UI layouts:

// List with avatar, title, subtitle
SkeletonPresets.listTile(itemCount: 5)

// E-commerce product card
SkeletonPresets.productCard(width: 160.0)

// Food delivery restaurant card
SkeletonPresets.foodCard()

// Horizontal scrollable card row
SkeletonPresets.horizontalCardRow(itemCount: 4)

Global Configuration

Set defaults for your entire app:

AutoSkeletonConfig(
  data: AutoSkeletonConfigData(
    baseColor: Color(0xFFE8E8E8),
    highlightColor: Color(0xFFF8F8F8),
    textBorderRadius: 4.0,
    containerBorderRadius: 8.0,
    enableSwitchAnimation: true,
  ),
  child: MaterialApp(...),
)

App-Level Wrapper (Zero Per-Widget Wrapping)

Wrap your entire app — every screen gets skeleton automatically:

final skeletonController = AutoSkeletonAppController();

MaterialApp(
  builder: AutoSkeletonApp.builder(
    controller: skeletonController,
  ),
  navigatorObservers: [skeletonController.observer],
  home: MyHomePage(),
)

Control it programmatically:

// Show skeleton
skeletonController.startLoading();

// Hide skeleton, show content
skeletonController.stopLoading();

// Toggle
skeletonController.toggle();

Or use a simple ValueNotifier:

final isLoading = ValueNotifier(true);

MaterialApp(
  builder: AutoSkeletonApp.builder(loadingNotifier: isLoading),
  home: MyHomePage(),
)

// Later...
isLoading.value = false; // skeleton disappears

Sliver Support

For use inside CustomScrollView:

CustomScrollView(
  slivers: [
    SliverAutoSkeleton(
      enabled: _isLoading,
      child: MyListContent(),
    ),
  ],
)

How It Works

  1. Layout Phase: The child widget tree is built and laid out (invisibly on the first frame).
  2. Scan Phase: WidgetTreeScanner walks the element tree and identifies content widgets.
  3. Bone Generation: For each content widget, a BoneRect is created matching its position and size.
  4. Paint Phase: BonePainter renders the chosen effect over each bone rectangle.
  5. Transition: When loading completes, the skeleton fades out and real content fades in.

Supported Widgets

The scanner automatically detects and creates bones for:

  • Text & RichText (with multi-line support)
  • Image, Icon, CircleAvatar
  • ElevatedButton, TextButton, OutlinedButton, IconButton
  • FloatingActionButton
  • Switch, Checkbox, Radio, Chip

Containers (Card, Container, Padding, etc.) are traversed to find their content children.

Example

Check the example directory for a complete demo app showing all features.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

BSD 3-Clause License
Copyright (c) 2026, Vaibhav Tambe

Libraries

auto_skeleton
Auto-generate skeleton/shimmer loading screens from your actual widget tree.