
Scroll-aware collapsing headers, parallax effects, animated collapsed titles, smooth background colour transitions, draggable tabbed scaffolds and a low-level collapsing layout for effortless polished scroll interactions.
Let’s be honest: writing custom scroll-aware animations is a pain. That’s I've we built scaffolds ( yes, with an s). It’s a KMP library designed to give your apps those high-end, polished interactions without the boilerplate.
Why you'll love it:
Scroll-Aware Magic: Effortless collapsing headers and parallax effects.
Truly Multiplatform: Native feel on iOS & Android, powerhouse performance on Desktop, and cutting-edge WASM support.
Mix & Match: Flexible components that fit your use case, not the other way around.
Stop reinventing the wheel and start building interactions that delight. 🎉
SimpleScaffold (with CollapsingLayout) |
CollapsibleScaffold |
|---|---|
![]() |
![]() |
Add the dependency to your build.gradle.kts:
implementation("com.dontsaybojio:scaffolds:0.0.1")Make sure mavenCentral() is in your repository list:
repositories {
mavenCentral()
}The library ships three components that build on top of each other. Pick the one that fits your needs!
A convenient wrapper that combines CollapsingLayout with a Material3 Scaffold. You bring your
own TopAppBar and expanded header — SimpleScaffold handles all the padding and scroll wiring for
you.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(navController: NavHostController) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
SimpleScaffold(
scrollBehavior = scrollBehavior,
topBar = {
TopAppBar(
title = { Text("My Screen") },
navigationIcon = { /* back button */ },
actions = { /* action icons */ }
)
},
expandedHeader = {
// The section that collapses as the user scrolls
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primary)
.padding(16.dp)
) {
Text(
text = "Aerith Gainsborough",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onPrimary
)
Text(
text = "A flower peddler living in the Sector 5 slums.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary
)
}
},
content = {
LazyColumn {
items(100) { index ->
ListItem(headlineContent = { Text("Item $index") })
}
}
}
)
}Parameters
| Parameter | Description |
|---|---|
scrollBehavior |
The TopAppBarScrollBehavior that drives the collapse. |
topBar |
Your TopAppBar composable. |
expandedHeader |
The header content that collapses on scroll. |
bottomBar |
Optional bottom bar composable. |
content |
The body content. |
The fully opinionated, batteries-included scaffold. It gives you:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(navController: NavHostController) {
val pagerState = rememberPagerState(pageCount = { tabs.size })
val scope = rememberCoroutineScope()
CollapsibleScaffold(
expandedHeaderBackground = MaterialTheme.colorScheme.tertiary,
collapsedHeaderBackground = MaterialTheme.colorScheme.secondary,
contentBackgroundColor = MaterialTheme.colorScheme.surface,
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actionIcon = {
IconButton(onClick = { }) {
Icon(Icons.Outlined.Info, contentDescription = "Info")
}
},
expandedHeader = { modifier ->
// The big header with parallax & fade applied via the provided modifier
Column(
modifier = modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Aerith Gainsborough",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onTertiary,
fontWeight = FontWeight.Bold
)
Text(
text = "A flower peddler living in the Sector 5 slums.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onTertiary
)
}
},
collapsedHeader = {
// Shown in the TopAppBar title area once the header has collapsed
Text(
text = "Aerith Gainsborough",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
tabView = {
// Tab row (or any sticky header) shown above the scrollable content
PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = Color.Transparent,
edgePadding = 0.dp
) {
tabs.forEachIndexed { index, tab ->
Tab(
selected = pagerState.currentPage == index,
text = { Text(tab.name) },
onClick = { scope.launch { pagerState.animateScrollToPage(index) } }
)
}
}
},
content = { scrollBehavior ->
// Pass the scrollBehavior down to your pager / list
HorizontalPager(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
state = pagerState
) { page ->
LazyColumn {
items(100) { index ->
ListItem(headlineContent = { Text("Item $index") })
}
}
}
}
)
}Parameters
| Parameter | Description |
|---|---|
expandedHeaderBackground |
Background colour when the header is fully expanded. |
collapsedHeaderBackground |
Background colour when the header is fully collapsed. The two colours smoothly lerp between each other. |
contentBackgroundColor |
Background colour of the rounded content card. |
cornerRadius |
Corner radius of the content card. Defaults to 16.dp. |
parallaxFactor |
How much the expanded header translates upward relative to the scroll. Defaults to 0.3f. |
navigationIcon |
Optional navigation icon (e.g. back button). |
actionIcon |
Optional action icon(s) in the TopAppBar. |
expandedHeader |
The large header content. Receives a Modifier that carries the parallax translation and fade — apply it to your root composable. |
collapsedHeader |
Content shown in the TopAppBar title area after the header has collapsed. |
tabView |
Sticky tab row (or any header) shown between the top bar and the scrollable content. |
content |
The scrollable body. Receives a TopAppBarScrollBehavior to pass to your list/pager via Modifier.nestedScroll. |
bottomBar |
Optional bottom bar composable. |
The low-level building block. It stacks a collapsible header on top of your body content and wires up the nested scroll so the header slides away as the user scrolls down — and comes back when they scroll up.
You can drive it with Material3's TopAppBarScrollBehavior (for the classic exit-until-collapsed
look) or let it manage the scroll state internally.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen() {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
CollapsingLayout(
scrollBehavior = scrollBehavior,
collapsingTop = {
// Your collapsible header goes here
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(MaterialTheme.colorScheme.primary)
)
},
bodyContent = {
// Your scrollable body goes here
LazyColumn {
items(100) { index ->
ListItem(headlineContent = { Text("Item $index") })
}
}
}
)
}Parameters
| Parameter | Description |
|---|---|
collapsingTop |
The collapsible header content. |
bodyContent |
The body content displayed below the header. |
modifier |
Modifier applied to the outer Box. |
scrollBehavior |
Optional TopAppBarScrollBehavior. When provided, its nestedScrollConnection drives the collapse. Defaults to an internal connection when null. |
| Tool | Version |
|---|---|
| Kotlin | 2.x |
| Compose Multiplatform | 1.x |
| Material3 | 1.x |
| Android min SDK | 23 |
CollapsingLayout inspired by Ayushi Gupta
CollapsibleScaffold inspired
by Dani Mahardhika
Let’s be honest: writing custom scroll-aware animations is a pain. That’s I've we built scaffolds ( yes, with an s). It’s a KMP library designed to give your apps those high-end, polished interactions without the boilerplate.
Why you'll love it:
Scroll-Aware Magic: Effortless collapsing headers and parallax effects.
Truly Multiplatform: Native feel on iOS & Android, powerhouse performance on Desktop, and cutting-edge WASM support.
Mix & Match: Flexible components that fit your use case, not the other way around.
Stop reinventing the wheel and start building interactions that delight. 🎉
SimpleScaffold (with CollapsingLayout) |
CollapsibleScaffold |
|---|---|
![]() |
![]() |
Add the dependency to your build.gradle.kts:
implementation("com.dontsaybojio:scaffolds:0.0.1")Make sure mavenCentral() is in your repository list:
repositories {
mavenCentral()
}The library ships three components that build on top of each other. Pick the one that fits your needs!
A convenient wrapper that combines CollapsingLayout with a Material3 Scaffold. You bring your
own TopAppBar and expanded header — SimpleScaffold handles all the padding and scroll wiring for
you.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(navController: NavHostController) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState()
)
SimpleScaffold(
scrollBehavior = scrollBehavior,
topBar = {
TopAppBar(
title = { Text("My Screen") },
navigationIcon = { /* back button */ },
actions = { /* action icons */ }
)
},
expandedHeader = {
// The section that collapses as the user scrolls
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primary)
.padding(16.dp)
) {
Text(
text = "Aerith Gainsborough",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onPrimary
)
Text(
text = "A flower peddler living in the Sector 5 slums.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary
)
}
},
content = {
LazyColumn {
items(100) { index ->
ListItem(headlineContent = { Text("Item $index") })
}
}
}
)
}Parameters
| Parameter | Description |
|---|---|
scrollBehavior |
The TopAppBarScrollBehavior that drives the collapse. |
topBar |
Your TopAppBar composable. |
expandedHeader |
The header content that collapses on scroll. |
bottomBar |
Optional bottom bar composable. |
content |
The body content. |
The fully opinionated, batteries-included scaffold. It gives you:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen(navController: NavHostController) {
val pagerState = rememberPagerState(pageCount = { tabs.size })
val scope = rememberCoroutineScope()
CollapsibleScaffold(
expandedHeaderBackground = MaterialTheme.colorScheme.tertiary,
collapsedHeaderBackground = MaterialTheme.colorScheme.secondary,
contentBackgroundColor = MaterialTheme.colorScheme.surface,
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actionIcon = {
IconButton(onClick = { }) {
Icon(Icons.Outlined.Info, contentDescription = "Info")
}
},
expandedHeader = { modifier ->
// The big header with parallax & fade applied via the provided modifier
Column(
modifier = modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Aerith Gainsborough",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onTertiary,
fontWeight = FontWeight.Bold
)
Text(
text = "A flower peddler living in the Sector 5 slums.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onTertiary
)
}
},
collapsedHeader = {
// Shown in the TopAppBar title area once the header has collapsed
Text(
text = "Aerith Gainsborough",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
tabView = {
// Tab row (or any sticky header) shown above the scrollable content
PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = Color.Transparent,
edgePadding = 0.dp
) {
tabs.forEachIndexed { index, tab ->
Tab(
selected = pagerState.currentPage == index,
text = { Text(tab.name) },
onClick = { scope.launch { pagerState.animateScrollToPage(index) } }
)
}
}
},
content = { scrollBehavior ->
// Pass the scrollBehavior down to your pager / list
HorizontalPager(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
state = pagerState
) { page ->
LazyColumn {
items(100) { index ->
ListItem(headlineContent = { Text("Item $index") })
}
}
}
}
)
}Parameters
| Parameter | Description |
|---|---|
expandedHeaderBackground |
Background colour when the header is fully expanded. |
collapsedHeaderBackground |
Background colour when the header is fully collapsed. The two colours smoothly lerp between each other. |
contentBackgroundColor |
Background colour of the rounded content card. |
cornerRadius |
Corner radius of the content card. Defaults to 16.dp. |
parallaxFactor |
How much the expanded header translates upward relative to the scroll. Defaults to 0.3f. |
navigationIcon |
Optional navigation icon (e.g. back button). |
actionIcon |
Optional action icon(s) in the TopAppBar. |
expandedHeader |
The large header content. Receives a Modifier that carries the parallax translation and fade — apply it to your root composable. |
collapsedHeader |
Content shown in the TopAppBar title area after the header has collapsed. |
tabView |
Sticky tab row (or any header) shown between the top bar and the scrollable content. |
content |
The scrollable body. Receives a TopAppBarScrollBehavior to pass to your list/pager via Modifier.nestedScroll. |
bottomBar |
Optional bottom bar composable. |
The low-level building block. It stacks a collapsible header on top of your body content and wires up the nested scroll so the header slides away as the user scrolls down — and comes back when they scroll up.
You can drive it with Material3's TopAppBarScrollBehavior (for the classic exit-until-collapsed
look) or let it manage the scroll state internally.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScreen() {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
CollapsingLayout(
scrollBehavior = scrollBehavior,
collapsingTop = {
// Your collapsible header goes here
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(MaterialTheme.colorScheme.primary)
)
},
bodyContent = {
// Your scrollable body goes here
LazyColumn {
items(100) { index ->
ListItem(headlineContent = { Text("Item $index") })
}
}
}
)
}Parameters
| Parameter | Description |
|---|---|
collapsingTop |
The collapsible header content. |
bodyContent |
The body content displayed below the header. |
modifier |
Modifier applied to the outer Box. |
scrollBehavior |
Optional TopAppBarScrollBehavior. When provided, its nestedScrollConnection drives the collapse. Defaults to an internal connection when null. |
| Tool | Version |
|---|---|
| Kotlin | 2.x |
| Compose Multiplatform | 1.x |
| Material3 | 1.x |
| Android min SDK | 23 |
CollapsingLayout inspired by Ayushi Gupta
CollapsibleScaffold inspired
by Dani Mahardhika