When you’re building long lists in Jetpack Compose, sometimes you need certain sections to stand out and stay visible while scrolling. That’s exactly where Sticky Header comes in. Imagine scrolling through a contacts app — the alphabet letter headers (A, B, C…) stick at the top while you browse through names. Jetpack Compose makes this easy with LazyColumn
and stickyHeader
.
In this guide, I’ll walk you through how to implement Sticky Header in Jetpack Compose with clear explanations.
What is a Sticky Header?
A Sticky Header is a UI element that “sticks” at the top of a scrollable list until the next header pushes it off. It’s commonly used in:
- Contact lists
- Calendar apps
- Shopping category lists
- News feeds with date separators
This improves navigation and makes large lists easier to scan.
Why Use Sticky Header in Jetpack Compose?
With Jetpack Compose, you don’t need RecyclerView adapters or complex custom views. The LazyColumn
component handles large, scrollable lists efficiently, and stickyHeader
makes adding sticky sections straightforward.
Benefits:
- Simple syntax, no XML layouts.
- Clean and declarative code.
- Works seamlessly with Compose state management.
LazyColumn and stickyHeader Basics
Here’s the basic structure of a LazyColumn
with a Sticky Header:
@Composable
fun StickyHeaderExample() {
val sections = listOf(
"Fruits" to listOf("Apple", "Banana", "Orange"),
"Vegetables" to listOf("Carrot", "Potato", "Tomato"),
"Dairy" to listOf("Milk", "Cheese", "Yogurt")
)
LazyColumn {
sections.forEach { (header, items) ->
stickyHeader {
Text(
text = header,
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(8.dp),
style = MaterialTheme.typography.subtitle1
)
}
items(items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
}
}
}
}
Let’s break it down:
Data Setup
val sections = listOf(
"Fruits" to listOf("Apple", "Banana", "Orange"),
"Vegetables" to listOf("Carrot", "Potato", "Tomato"),
"Dairy" to listOf("Milk", "Cheese", "Yogurt")
)
Here, each section has a header (like “Fruits”) and a list of items.
LazyColumn
LazyColumn { ... }
Displays the entire list efficiently. Only visible items are composed, so it’s memory-friendly.
stickyHeader
stickyHeader {
Text(
text = header,
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(8.dp)
)
}
This is the star of the show. The header stays pinned at the top while scrolling through its section.
items()
items(items) { item -> ... }
Renders each element under the sticky header.
Customizing Sticky Headers
You can style sticky headers to fit your app’s design. For example:
- Add icons to headers.
- Change background color based on section.
- Apply elevation or shadows for better separation.
Example with custom styling:
stickyHeader {
Surface(
color = Color.DarkGray,
shadowElevation = 4.dp
) {
Text(
text = header,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
color = Color.White,
style = MaterialTheme.typography.h6
)
}
}
When to Use and When Not To
Use Sticky Header when:
- The list is grouped (categories, dates, sections).
- Users need quick context while scrolling.
Avoid Sticky Header if:
- The list is flat (no categories).
- Too many headers clutter the UI.
Performance Considerations
LazyColumn
with stickyHeader
is optimized, but keep these in mind:
- Keep headers lightweight (avoid heavy Composables inside).
- Reuse stateful items outside of the list when possible.
- Test on lower-end devices if you have very large datasets.
Conclusion
The Sticky Header in Jetpack Compose makes complex, sectioned lists much easier to build and navigate. With just a few lines of code inside a LazyColumn
, you can create polished, user-friendly experiences without dealing with RecyclerView boilerplate.
If you’re building apps with grouped data — contacts, shopping categories, or event timelines — Sticky Header is a feature you’ll definitely want to use.