Question #346MediumWidgets & UI

How to create a list with persistent headers?

#list#headers#slivers

Answer

Overview

A list with persistent headers (sticky headers) can be implemented using Slivers in Flutter — specifically

text
SliverList
and
text
SliverPersistentHeader
inside a
text
CustomScrollView
.


Using SliverList + SliverPersistentHeader

dart
class StickyHeaderList extends StatelessWidget {
  final Map<String, List<String>> sections = {
    'A': ['Alice', 'Aaron', 'Amy'],
    'B': ['Bob', 'Brian', 'Betty'],
    'C': ['Carol', 'Chris', 'Clara'],
  };

  
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        for (final entry in sections.entries) ...[
          // Sticky Header
          SliverPersistentHeader(
            pinned: true,
            delegate: _SectionHeaderDelegate(entry.key),
          ),
          // Items under this header
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(title: Text(entry.value[index])),
              childCount: entry.value.length,
            ),
          ),
        ],
      ],
    );
  }
}

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

  
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Colors.blue[100],
      alignment: Alignment.centerLeft,
      padding: EdgeInsets.symmetric(horizontal: 16),
      child: Text(
        title,
        style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
      ),
    );
  }

  
  double get maxExtent => 48;

  
  double get minExtent => 48;

  
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => false;
}

Using sticky_headers Package (Easier)

dart
import 'package:sticky_headers/sticky_headers.dart';

ListView.builder(
  itemCount: sections.length,
  itemBuilder: (context, index) {
    final section = sections[index];
    return StickyHeader(
      header: Container(
        height: 50,
        color: Colors.blueGrey[700],
        alignment: Alignment.centerLeft,
        padding: EdgeInsets.symmetric(horizontal: 16),
        child: Text(section.title, style: TextStyle(color: Colors.white)),
      ),
      content: Column(
        children: section.items.map((item) => ListTile(title: Text(item))).toList(),
      ),
    );
  },
)

Pinned vs Floating Header

dart
SliverPersistentHeader(
  pinned: true,    // Header stays at top while scrolling past it
  floating: false, // Header comes back only when scrolled to its position
  delegate: MyHeaderDelegate(),
)
OptionBehavior
text
pinned: true
Sticks at top of viewport
text
floating: true
Reappears when scrolling back up
text
pinned: true, floating: true
Sticks + reappears quickly

Recommendation: For most contact-list or alphabetical list UIs, use

text
pinned: true
with
text
SliverPersistentHeader
. For a simpler solution, use the
text
sticky_headers
package.