← Articles

Sliver widgets in Flutter explained

By Ann Tech · 24 July 2025

Sliver widgets are the foundation of Flutter's scroll performance. Every ListView, GridView, and CustomScrollView is built on top of them. Understanding slivers lets you build scroll experiences that are impossible with regular widgets — collapsing headers, mixed content, pinned sections — without fighting the framework.

What is a sliver?

A sliver is a widget that participates in a CustomScrollView's scroll model. Unlike a regular widget that has a fixed size, a sliver communicates with the scroll controller through a SliverConstraintsSliverGeometry protocol:

  • SliverConstraints tell the sliver: here's how much space is available, here's the scroll offset, here's the overlap from above.
  • SliverGeometry is the sliver's response: here's how big I am, here's how much I painted, here's whether I want to be pinned.

This protocol enables the scroll position to drive layout — things regular Column + SingleChildScrollView can't do.

The basics: CustomScrollView

CustomScrollView(
  slivers: [
    const SliverAppBar(
      title: Text('Orders'),
      floating: true,
      snap: true,
    ),
    SliverPadding(
      padding: const EdgeInsets.all(16),
      sliver: SliverList.builder(
        itemCount: orders.length,
        itemBuilder: (context, index) => OrderCard(order: orders[index]),
      ),
    ),
  ],
)

Everything inside slivers: must be a sliver. You can't put a Container directly — wrap it in SliverToBoxAdapter.

SliverAppBar: collapsing headers

SliverAppBar(
  expandedHeight: 200,
  floating: false,   // Doesn't return until scrolled to top
  pinned: true,      // Title stays visible when collapsed
  snap: false,
  flexibleSpace: FlexibleSpaceBar(
    title: const Text('My Orders'),
    background: Image.network(
      'https://example.com/header.jpg',
      fit: BoxFit.cover,
    ),
    collapseMode: CollapseMode.parallax,
  ),
  actions: [
    IconButton(icon: const Icon(Icons.search), onPressed: () {}),
  ],
)

pinned: true + expandedHeight gives the classic collapsing header effect where the image shrinks to a regular app bar as the user scrolls.

SliverPersistentHeader: custom pinned sections

class _SectionHeaderDelegate extends SliverPersistentHeaderDelegate {
  const _SectionHeaderDelegate(this.title);
  final String title;

  @override
  double get minExtent => 40;
  @override
  double get maxExtent => 40;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return Container(
      color: Colors.white,
      alignment: Alignment.centerLeft,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Text(
        title,
        style: const TextStyle(fontWeight: FontWeight.bold),
      ),
    );
  }

  @override
  bool shouldRebuild(_SectionHeaderDelegate old) => old.title != title;
}

// In CustomScrollView:
SliverPersistentHeader(
  pinned: true,
  delegate: _SectionHeaderDelegate('Pending Orders'),
)

Mixed content scrolling

CustomScrollView(
  slivers: [
    // Collapsing banner
    SliverAppBar(
      expandedHeight: 180,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        background: Image.asset('assets/banner.jpg', fit: BoxFit.cover),
      ),
    ),

    // Pinned filter bar
    SliverPersistentHeader(
      pinned: true,
      delegate: _FilterBarDelegate(),
    ),

    // Horizontal carousel (converted to sliver)
    SliverToBoxAdapter(
      child: SizedBox(
        height: 120,
        child: ListView.builder(
          scrollDirection: Axis.horizontal,
          itemCount: featuredItems.length,
          itemBuilder: (_, i) => FeaturedCard(item: featuredItems[i]),
        ),
      ),
    ),

    // Section header
    SliverPersistentHeader(
      pinned: true,
      delegate: _SectionHeaderDelegate('All Orders'),
    ),

    // Main list
    SliverList.builder(
      itemCount: orders.length,
      itemBuilder: (_, i) => OrderCard(order: orders[i]),
    ),

    // Footer padding
    const SliverPadding(padding: EdgeInsets.only(bottom: 80)),
  ],
)

SliverGrid

SliverGrid(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 3 / 2,
    mainAxisSpacing: 8,
    crossAxisSpacing: 8,
  ),
  delegate: SliverChildBuilderDelegate(
    (context, index) => ProductCard(product: products[index]),
    childCount: products.length,
  ),
)

For adaptive grid columns based on available width:

SliverGrid(
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 200, // Each cell is at most 200px wide
    childAspectRatio: 1,
    mainAxisSpacing: 8,
    crossAxisSpacing: 8,
  ),
  delegate: SliverChildBuilderDelegate(
    (context, index) => ProductCard(product: products[index]),
    childCount: products.length,
  ),
)

Efficient lazy loading with SliverList.builder

SliverList.builder only builds items as they scroll into view:

SliverList.builder(
  itemCount: orders.length + (isLoading ? 1 : 0),
  itemBuilder: (context, index) {
    if (index == orders.length) {
      return const Center(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: CircularProgressIndicator(),
        ),
      );
    }
    return OrderCard(order: orders[index]);
  },
)

SliverAnimatedList

For animated insertions and removals:

final _listKey = GlobalKey<SliverAnimatedListState>();

void insertItem(int index) {
  orders.insert(index, newOrder);
  _listKey.currentState!.insertItem(
    index,
    duration: const Duration(milliseconds: 300),
  );
}

void removeItem(int index) {
  final removed = orders.removeAt(index);
  _listKey.currentState!.removeItem(
    index,
    (context, animation) => SizeTransition(
      sizeFactor: animation,
      child: OrderCard(order: removed),
    ),
  );
}

// In CustomScrollView:
SliverAnimatedList(
  key: _listKey,
  itemBuilder: (context, index, animation) => SizeTransition(
    sizeFactor: animation,
    child: OrderCard(order: orders[index]),
  ),
)

Common pitfalls

Putting a regular widget directly in slivers. You'll get a runtime error: "A RenderSliver expected a child of type RenderSliver." Wrap with SliverToBoxAdapter.

Nested scroll views without coordination. If you put a ListView inside a CustomScrollView, it has its own scroll controller and the two fight each other. Use SliverList instead.

Setting shrinkWrap: true on a ListView inside a CustomScrollView. This forces the ListView to measure all children at once, killing lazy loading. Use SliverList.builder instead.

Forgetting SliverPadding. Adding Padding directly in slivers doesn't work — use SliverPadding to wrap other slivers.

Sign in to like, dislike, or report.

Sliver widgets in Flutter explained — ANN Tech