Skip to content
Boring Doc
Official documentation (snapshot)doc

Advanced scrolling and slivers

Learn how to implement performant scrolling with slivers.

Source last updated Oct 21, 2026 · last synchronized 3 weeks ago · commit b3045439b7c0 · read on docs.flutter.dev

This is a rendered snapshot of the official page as stored by Boring Doc for provenance and search. Site-specific components (embedded demos, DartPad, images with relative paths) may not render; use the official page for the full experience. Content © The Flutter Authors, CC BY 3.0.

In this lesson, you'll learn about slivers, which are special widgets that can take advantage of Flutter's powerful and composable scrolling system. Slivers enable you to create sophisticated scroll effects, including collapsible headers, search integration, and custom scroll behaviors. By the end of this section, you'll understand how to use CustomScrollView, create navigation bars that collapse, and organize content in scrollable sections.


Slivers and widgets

Slivers are scrollable areas that can be composed together in a CustomScrollView or other scroll views. Think of slivers as building blocks that each contribute a portion of the overall scrollable content.

While slivers and widgets are both fundamental Flutter concepts, they serve different purposes and aren't interchangeable.

  • Widgets are general UI building blocks that can be used anywhere in your widget tree.

  • Slivers are specialized widgets designed specifically for scrollable layouts and have some constraints:

  • Slivers can only be direct children of scroll views, such as CustomScrollView and NestedScrollView.

  • Some scroll views only accept slivers as children. You can't pass regular widgets to CustomScrollView.slivers.

  • To use regular widgets within a sliver context, wrap them in SliverToBoxAdapter or SliverFillRemaining.

This architectural separation allows Flutter to optimize scrolling performance while it maintains clear boundaries between different types of UI components.

Add a basic sliver structure to contact groups

First, replace the placeholder content in your contact groups page. To avoid duplicating code between the phone layout and the tablet sidebar, you can create a private, reusable widget.

Update lib/screens/contact_groups.dart by adding _ContactGroupsView to the bottom of the file.

import 'package:cupertino_ui/cupertino_ui.dart';

import '../data/contact_group.dart';
import '../main.dart';

class ContactGroupsPage extends StatelessWidget {
  const ContactGroupsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      selectedListId: 0,
      onListSelected: (list) {
        debugPrint(list.toString());
      },
    );
  }
}

// ···
class _ContactGroupsView extends StatelessWidget {
  const _ContactGroupsView({required this.onListSelected, this.selectedListId});

  final int? selectedListId;
  final void Function(ContactGroup) onListSelected;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      backgroundColor: CupertinoColors.extraLightBackgroundGray,
      child: CustomScrollView(
        slivers: [
          const CupertinoSliverNavigationBar(largeTitle: Text('Lists')),
          SliverFillRemaining(
            child: ValueListenableBuilder<List<ContactGroup>>(
              valueListenable: contactGroupsModel.listsNotifier,
              builder: (context, contactLists, child) {
                return CupertinoListSection.insetGrouped(
                  header: const Text('iPhone'),
                  children: [
                    for (final ContactGroup contactList in contactLists)
                      CupertinoListTile(
                        title: Text(contactList.label),
                        onTap: () => onListSelected(contactList),
                      ),
                  ],
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

This private widget contains the shared UI for displaying the list of contact groups. On small screens, it will be used as a page, and on large screens it will be used to fill the left column.

This widget introduces several slivers:

  • CupertinoSliverNavigationBar: An opinionated navigation bar that collapses as the page scrolls.
  • SliverList: A scrollable list of items.
  • SliverFillRemaining: A sliver that takes up the remaining space in the scroll area, and whose child is a non-sliver widget.

It accepts a callback function, onListSelected, to handle taps, which makes it adaptable for both navigation and sidebar selection.

Now, update ContactGroupsPage in lib/screens/contact_groups.dart to use your new _ContactGroupsView widget:

class ContactGroupsPage extends StatelessWidget {
  const ContactGroupsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return _ContactGroupsView(
      selectedListId: 0,
      onListSelected: (list) {
        debugPrint(list.toString());
      },
    );
  }
}

This structure keeps the ContactGroupsPage clean and focused on its primary responsibility: navigation, which you'll learn about in the next section of this tutorial.

Enhance the list with icons and visual elements

Now, add icons and contact counts to make the list more informative. Add this _buildTrailing helper method to your _ContactGroupsView class in lib/screens/contact_groups.dart:

Widget _buildTrailing(List<Contact> contacts, BuildContext context) {
  final TextStyle style = CupertinoTheme.of(context).textTheme.textStyle
      .copyWith(color: CupertinoColors.systemGrey);

  return Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Text(contacts.length.toString(), style: style),
      const Icon(
        CupertinoIcons.forward,
        color: CupertinoColors.systemGrey3,
        size: 18,
      ),
    ],
  );
}

This helper creates the trailing content for each list item. It shows the contact count and a forward arrow.

Now, update the CupertinoListSection in _ContactGroupsView to use icons and the trailing helper. Update the code within the ValueListenableBuilder.builder callback in the build method:

child: ValueListenableBuilder<List<ContactGroup>>(
  valueListenable: contactGroupsModel.listsNotifier,
  builder: (context, contactLists, child) {
    const groupIcon = Icon(
      CupertinoIcons.group,
      weight: 900,
      size: 32,
    );

    const pairIcon = Icon(
      CupertinoIcons.person_2,
      weight: 900,
      size: 24,
    );

    return CupertinoListSection.insetGrouped(
      header: const Text('iPhone'),
      children: [
        for (final ContactGroup contactList in contactLists)
          CupertinoListTile(
            leading: contactList.id == 0 ? groupIcon : pairIcon,
            title: Text(contactList.label),
            trailing: _buildTrailing(contactList.contacts, context),
            onTap: () => onListSelected(contactList),
          ),
      ],
    );
  },
),

The updated code now shows icons that differentiate between the main "All iPhone" group and user-created groups, along with contact counts and navigation indicators.

Create advanced scrolling for contacts

Next, you'll implement the contacts list page.

In the next lesson, you'll implement navigation for small screens. To see your progress on the contacts list page in the meantime, first update lib/screens/adaptive_layout.dart to display the contacts list page:


import 'package:cupertino_ui/cupertino_ui.dart';

import 'contacts.dart';

const largeScreenMinWidth = 600;

class AdaptiveLayout extends StatefulWidget {
  const AdaptiveLayout({super.key});

  @override
  State<AdaptiveLayout> createState() => _AdaptiveLayoutState();
}

class _AdaptiveLayoutState extends State<AdaptiveLayout> {
  int selectedListId = 0;

  void _onContactListSelected(int listId) {
    setState(() {
      selectedListId = listId;
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isLargeScreen = constraints.maxWidth > largeScreenMinWidth;

        if (isLargeScreen) {
          return _buildLargeScreenLayout();
        } else {
          return const ContactListsPage(listId: 0); // New, temporary
        }
      },
    );
  }

  Widget _buildLargeScreenLayout() {
    return CupertinoPageScaffold(
      backgroundColor: CupertinoColors.extraLightBackgroundGray,
      child: SafeArea(
        child: Row(
          children: [
            const SizedBox(width: 320, child: Text('Sidebar placeholder')),
            Container(width: 1, color: CupertinoColors.separator),
            const Expanded(child: Text('Details placeholder')),
          ],
        ),
      ),
    );
  }
}

Update lib/screens/contacts.dart by adding _ContactListView to the bottom of the file:

class _ContactListView extends StatelessWidget {
  const _ContactListView({
    required this.listId,
    this.automaticallyImplyLeading = true,
  });

  final int listId;
  final bool automaticallyImplyLeading;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: ValueListenableBuilder<List<ContactGroup>>(
        valueListenable: contactGroupsModel.listsNotifier,
        builder: (context, contactGroups, child) {
          final contactList = contactGroupsModel.findContactList(listId);

          return CustomScrollView(
            slivers: [
              CupertinoSliverNavigationBar(
                largeTitle: Text(contactList.title),
                automaticallyImplyLeading: automaticallyImplyLeading,
              ),
              SliverFillRemaining(
                child: Center(
                  child: Text(
                    '${contactList.contacts.length} contacts in ${contactList.label}',
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

Now, update ContactListsPage to use this view:

class ContactListsPage extends StatelessWidget {
  const ContactListsPage({super.key, required this.listId});

  final int listId;

  @override
  Widget build(BuildContext context) {
    return _ContactListView(listId: listId);
  }
}

This basic implementation demonstrates how to use slivers with dynamic data in a reusable component.

Add search integration with slivers

Now, enhance the contacts page with integrated search functionality UI. Update the CustomScrollView in _ContactListView to use the CupertinoSliverNavigationBar.search constructor instead of the default CupertinoSliverNavigationBar constructor:

class _ContactListView extends StatelessWidget {
  // ···
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: ValueListenableBuilder<List<ContactGroup>>(
        valueListenable: contactGroupsModel.listsNotifier,
        builder: (context, contactGroups, child) {
          final contactList = contactGroupsModel.findContactList(listId);

          return CustomScrollView(
            slivers: [
              // Now using a search bar:
              CupertinoSliverNavigationBar.search(
                largeTitle: Text(contactList.title),
                searchField: const CupertinoSearchTextField(
                  suffixIcon: Icon(CupertinoIcons.mic_fill),
                  suffixMode: OverlayVisibilityMode.always,
                ),
              ),
              SliverFillRemaining(
                child: Center(
                  child: Text(
                    '${contactList.contacts.length} contacts in ${contactList.label}',
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

The CupertinoSliverNavigationBar.search constructor provides integrated search functionality. As you scroll down, the search field smoothly transitions into the collapsed navigation bar.

Create alphabetized contact sections

Real-world contact apps organize contacts alphabetically. To do this, create sections for each letter. Add the following widget to the bottom of your contacts.dart file. This widget doesn't contain any slivers.

class ContactListSection extends StatelessWidget {
  const ContactListSection({
    super.key,
    required this.lastInitial,
    required this.contacts,
  });

  final String lastInitial;
  final List<Contact> contacts;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsetsDirectional.fromSTEB(20, 0, 20, 0),
      child: Column(
        children: [
          const SizedBox(height: 15),
          Align(
            alignment: AlignmentDirectional.bottomStart,
            child: Text(
              lastInitial,
              style: const TextStyle(
                color: CupertinoColors.systemGrey,
                fontSize: 15,
                fontWeight: FontWeight.w700,
              ),
            ),
          ),
          CupertinoListSection(
            backgroundColor: CupertinoColors.systemBackground,
            dividerMargin: 0,
            additionalDividerMargin: 0,
            topMargin: 4,
            children: [
              for (final Contact contact in contacts)
                CupertinoListTile(
                  padding: const EdgeInsets.all(0),
                  title: Text('${contact.firstName} ${contact.lastName}'),
                ),
            ],
          ),
        ],
      ),
    );
  }
}

This widget creates the familiar alphabetized sections that you see in the iOS Contacts app.

Use SliverList for the alphabetized sections

Now, replace the placeholder content in _ContactListView with the alphabetized sections:

class _ContactListView extends StatelessWidget {
  // ···
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: ValueListenableBuilder<List<ContactGroup>>(
        valueListenable: contactGroupsModel.listsNotifier,
        builder: (context, contactGroups, child) {
          final contactList = contactGroupsModel.findContactList(listId);

          final contacts = contactList.alphabetizedContacts;

          return CustomScrollView(
            slivers: [
              CupertinoSliverNavigationBar.search(
                largeTitle: Text(contactList.title),
                automaticallyImplyLeading: automaticallyImplyLeading,
                searchField: const CupertinoSearchTextField(
                  suffixIcon: Icon(CupertinoIcons.mic_fill),
                  suffixMode: OverlayVisibilityMode.always,
                ),
              ),
              SliverList.list(
                children: [
                  const SizedBox(height: 20),
                  ...contacts.keys.map(
                    (initial) => ContactListSection(
                      lastInitial: initial,
                      contacts: contacts[initial]!,
                    ),
                  ),
                ],
              ),
            ],
          );
        },
      ),
    );
  }
}

SliverList.list allows you to provide a list of widgets that become part of the scrollable content. This is the simplest way to add a list of normal widgets to a scrollable sliver area.

In the next lesson, you'll learn about stack-based navigation and update the UI on small screens to navigate between the contacts list view and the contacts view.

Review

Test yourself

Was this useful?

Content from flutter/website, © The Flutter Authors, used under the Creative Commons Attribution 3.0 License; code samples under the BSD License.

Advanced scrolling and slivers (official source) · Boring Doc