Skip to content
Blog

CustomScrollView and Slivers: Building Collapsible, Layered Scroll UIs in Flutter

A practical guide to Flutter slivers: CustomScrollView, SliverAppBar collapsing headers, SliverList and SliverGrid, SliverToBoxAdapter, SliverPersistentHeader, NestedScrollView, and the performance wins of lazy sliver layout.

Published on • September 25, 2026

AI Assistant

A ListView gives you one scrollable list. Real screens are rarely one scrollable list. They are a photo header that collapses as you scroll, a grid of categories, a pinned section title, then an infinite feed — all in a single scroll gesture, with no jank. You cannot build that by nesting a ListView inside a ListView, and gluing widgets over a scroll view with animated offsets is a maintenance nightmare.

Slivers are Flutter’s answer. A sliver is a portion of a scrollable area with its own layout behavior. Instead of laying out whole boxes, a sliver receives scroll offsets and can size, position, and paint itself based on where the user is. Compose them in a CustomScrollView and collapsing headers, sticky sections, and mixed list/grid layouts become declarative — the scroll pipeline does the work.

CustomScrollView: The Sliver Container

CustomScrollView takes a list of slivers and stitches them into one scroll view:

CustomScrollView(
  slivers: [
    SliverAppBar(...),
    SliverList(...),
    SliverGrid(...),
  ],
)

The relationship runs deeper than syntax: a ListView is internally a CustomScrollView containing exactly one list sliver. CustomScrollView is not a different rendering system — it is the same system with the single-sliver restriction lifted, which is why everything composes correctly in one scroll coordinate space.

SliverAppBar: Collapsing Headers

The most-used sliver gets its own treatment because its flags are frequently confused:

SliverAppBar(
  expandedHeight: 220,
  pinned: true,
  floating: false,
  snap: false,
  flexibleSpace: FlexibleSpaceBar(
    title: const Text('Fancy Scrolling'),
    background: Image.asset('header.jpg', fit: BoxFit.cover),
  ),
)
  • pinned: true — the bar shrinks but never leaves the screen. The classic “collapsing toolbar” pattern: expanded photo header at rest, compact toolbar after scrolling.
  • floating: true — the bar reappears immediately on any upward scroll, without waiting for the list to reach the top. Good for search-driven feeds.
  • snap: true — requires floating; any upward scroll expands the bar fully in one animated snap, rather than tracking the finger.
  • expandedHeight + flexibleSpace — the space that collapses. FlexibleSpaceBar handles the standard parallax background and title-scaling behavior.

SliverAppBar is itself built on SliverPersistentHeader, which you can use directly for custom collapsing headers with a delegate controlling min/max extent and shrink/overlay behavior.

Lists and Grids as Slivers

SliverList and SliverGrid are the lazy workhorses. Use the .builder delegate constructors to build children on demand:

SliverGrid(
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 200.0,
    mainAxisSpacing: 8.0,
    crossAxisSpacing: 8.0,
    childAspectRatio: 1.0,
  ),
  delegate: SliverChildBuilderDelegate(
    (context, index) => ItemTile(index),
    childCount: 1000,
  ),
)

For long uniform lists, SliverFixedExtentList takes an itemExtent and unlocks a further optimization: because every child’s extent is known ahead of time, the viewport can jump directly to a scroll offset without laying out intermediate children — fast scrolling through a hundred thousand rows.

The Utility Slivers

  • SliverToBoxAdapter — wraps an ordinary box widget (a header, a banner, a divider) so it can live inside a CustomScrollView. The bridge between box world and sliver world.
  • SliverPadding — applies EdgeInsets as a sliver. Wrapping the whole scroll view in a Padding instead changes the viewport geometry and breaks the slivers inside — padding must be applied in sliver space.
  • SliverFillRemaining — occupies the rest of the viewport regardless of content size. Natural fit for empty states and full-screen “load more” footers.
  • SliverOpacity, SliverIgnorePointer, SliverLayoutBuilder, SliverAnimatedOpacity, SliverFadeTransition — sliver versions of their box counterparts, so you can animate or gate a whole scroll section without reparenting it into a box.

A Complete Screen

Putting it together — collapsing header, section label, grid, then a long fixed-extent list, all in one scroll:

CustomScrollView(
  slivers: [
    SliverAppBar(
      expandedHeight: 220,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        title: const Text('Fancy Scrolling'),
        background: Image.asset('header.jpg', fit: BoxFit.cover),
      ),
    ),
    const SliverToBoxAdapter(child: SectionHeader('Latest')),
    SliverPadding(
      padding: const EdgeInsets.all(8),
      sliver: SliverGrid(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisExtent(
          crossAxisCount: 2,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
        ),
        delegate: SliverChildBuilderDelegate(
          (_, i) => Card(child: Center(child: Text('Item $i'))),
          childCount: 20,
        ),
      ),
    ),
    SliverFixedExtentList(
      itemExtent: 60,
      delegate: SliverChildBuilderDelegate(
        (_, i) => ListTile(title: Text('Row $i')),
        childCount: 100,
      ),
    ),
  ],
)

No Column with Expanded, no nested scroll controllers, no manual header offset math — one scroll position, every effect native to the layout system.

NestedScrollView for Header + Tab Body

The pattern “collapsing header on top, TabBarView of lists below” has a dedicated tool: NestedScrollView coordinates an outer scrollable (typically a SliverAppBar) with inner body scrollables so the header collapses first, then the inner list scrolls — while preserving a single overall scroll position. The inner lists must use the controllers NestedScrollView provides, and SliverOverlapAbsorberHandle keeps pinned tab bars from sliding under the collapsing header. The Flutter docs cover this pattern in detail; the key mistake to avoid is giving the inner lists their own ScrollController.

CustomScrollView vs ListView: When to Use Which

Use a plain ListView/GridView for a simple, homogeneous list — less code, identical lazy behavior. Reach for CustomScrollView when you need:

  • A collapsing, floating, or pinned app bar over content
  • Multiple sections of different kinds (box + grid + list) in one scroll
  • Sticky section headers or scroll-position-dependent layout
  • Effects built in the scroll pipeline instead of MediaQuery-driven rebuild hacks or external AnimationControllers

The performance argument is real as well: lazy building with .builder delegates constructs only visible children, and fixed-extent slivers enable offset-based skipping. For very long or jump-heavy lists, slivers are not just more capable — they are faster.

Tips from the Trenches

  • shrinkWrap: true on a ListView inside a CustomScrollView is a smell. You already have a sliver viewport; convert the list to SliverList instead of nesting a second scrollable.
  • Always prefer .builder delegates in slivers — the non-builder constructors build every child eagerly.
  • Parallax and floating app bars have cookbook recipes (“Place a floating app bar above a list”, “Create a scrolling parallax effect”) — steal from those before inventing geometry.
  • For deep understanding, the community references are “Slivers, Demystified” and the “Slivers explained” Boring Show episode with Ian Hickson, plus the API docs for CustomScrollView, SliverAppBar, and SliverGrid.

Summary

Slivers are the difference between scrolling as a container and scrolling as a layout system. Once you think in slivers, the screens that felt impossible — photo headers dissolving into pinned toolbars, grids flowing into infinite feeds, sections that fade as they leave — collapse into a declarative list of sliver widgets with the framework handling all the offset math. Start with SliverAppBar over a SliverList, and you will never go back to nested scroll views.

References