Flutter Motion Kit

Previewable Flutter animations + pitfall guide · one-click reuse from Claude Code

← Back

Collapsing SliverAppBar (parallax)

explicit · difficulty 3/5 · verified Flutter 3.32 / 2026-06

A scroll-driven collapsing header: a SliverAppBar with expandedHeight and a FlexibleSpaceBar whose background parallaxes while the bar shrinks to a pinned toolbar. No AnimationController — the scroll offset is the driver.

▶︎ Running for real (Flutter 3.32 · self-hosted, not a recording)

Code

// ✅ Recommended: a SliverAppBar inside a CustomScrollView, with a FlexibleSpaceBar
// whose background parallaxes as the bar collapses to a pinned toolbar.
// The scroll offset drives everything — no AnimationController needed.
// Paste straight into DartPad (https://dartpad.dev) to run.
import 'package:flutter/material.dart';

void main() => runApp(const _App());

class _App extends StatelessWidget {
  const _App();
  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
    home: const _Demo(),
  );
}

class _Demo extends StatelessWidget {
  const _Demo();

  static const _items = [
    'Overview',
    'Activity',
    'Photos',
    'Files',
    'Members',
    'Settings',
    'Billing',
    'Integrations',
    'Notifications',
    'Security',
    'Advanced',
    'About',
  ];

  @override
  Widget build(BuildContext context) {
    // ✅ A SliverAppBar must live inside a sliver-aware viewport.
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            // pinned: keep a collapsed toolbar on screen instead of letting it scroll away.
            pinned: true,
            expandedHeight: 240,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('Collapsing header'),
              // ✅ parallax makes the background move slower than the title as it collapses.
              collapseMode: CollapseMode.parallax,
              background: DecoratedBox(
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [Color(0xFF3F51B5), Color(0xFF7E57C2)],
                  ),
                ),
                child: Align(
                  alignment: Alignment.bottomRight,
                  child: Padding(
                    padding: const EdgeInsets.all(24),
                    child: Icon(
                      Icons.landscape,
                      size: 96,
                      color: Colors.white.withValues(alpha: 0.25),
                    ),
                  ),
                ),
              ),
            ),
          ),
          // ✅ Body shares the same scroll via a SliverList, not a nested ListView.
          SliverList(
            delegate: SliverChildBuilderDelegate((context, i) {
              return ListTile(
                leading: CircleAvatar(child: Text('${i + 1}')),
                title: Text(_items[i]),
                subtitle: const Text('Scroll up to collapse the header'),
              );
            }, childCount: _items.length),
          ),
        ],
      ),
    );
  }
}

⚠️ Pitfalls (4)

A SliverAppBar is a sliver and only works inside a viewport that speaks the sliver protocol; dropping it in a Column or a box-based ListView throws or fails to render.
✅ Put SliverAppBar inside a CustomScrollView's slivers list (or a NestedScrollView headerSliverBuilder).
official-docs · source
Without pinned (or floating/snap) the app bar scrolls completely off-screen and never returns until you scroll back to the top.
✅ Choose the behavior deliberately — set pinned:true to keep a collapsed toolbar visible, floating:true to bring it back on any upward scroll.
official-docs · source
If expandedHeight is too small the FlexibleSpaceBar title overlaps the background and the collapse looks cramped or janky.
✅ Give expandedHeight enough room (e.g. ~240) and use CollapseMode.parallax so the background moves slower than the foreground.
official-docs · source
Nesting a regular ListView for the body inside the CustomScrollView gives an unbounded-height error or a second, conflicting scroll view.
✅ Use a SliverList with SliverChildBuilderDelegate (or SliverToBoxAdapter) so the body shares the single CustomScrollView scroll.
official-docs · source

Official docs