TabLayout in Compose — ScrollableTabRow & HorizontalPager
Jetpack Compose provides powerful tab navigation with TabRow and ScrollableTabRow. Let's explore the essential patterns for creating responsive, swipeable tab layouts.
Basic TabRow with 3 Tabs
The simplest implementation with static tabs:
@Composable
fun BasicTabLayout() {
var selectedTabIndex by remember { mutableIntStateOf(0) }
val tabs = listOf("Home", "Explore", "Profile")
TabRow(selectedTabIndex = selectedTabIndex) {
tabs.forEachIndexed { index, title ->
Tab(
selected = index == selectedTabIndex,
onClick = { selectedTabIndex = index },
text = { Text(title) }
)
}
}
}
Icon + Badge Tabs
Combine icons with optional badge indicators:
@Composable
fun IconBadgeTabRow() {
var selectedTabIndex by remember { mutableIntStateOf(0) }
TabRow(selectedTabIndex = selectedTabIndex) {
Tab(
selected = selectedTabIndex == 0,
onClick = { selectedTabIndex = 0 }
) {
BadgedBox(badge = { Badge { Text("3") } }) {
Icon(Icons.Filled.Home, contentDescription = "Home")
}
}
Tab(
selected = selectedTabIndex == 1,
onClick = { selectedTabIndex = 1 },
text = { Text("Messages") },
icon = { Icon(Icons.Filled.Mail, contentDescription = "Messages") }
)
}
}
ScrollableTabRow for Many Tabs
When tabs exceed screen width, use ScrollableTabRow:
@Composable
fun ScrollableTabLayoutDemo() {
var selectedTabIndex by remember { mutableIntStateOf(0) }
val tabs = (1..15).map { "Tab $it" }
ScrollableTabRow(selectedTabIndex = selectedTabIndex) {
tabs.forEachIndexed { index, title ->
Tab(
selected = index == selectedTabIndex,
onClick = { selectedTabIndex = index },
text = { Text(title) }
)
}
}
}
TabRow + HorizontalPager Swipe Integration
Synchronize tabs with swipeable page content:
@Composable
fun TabbedPagerLayout() {
var selectedTabIndex by remember { mutableIntStateOf(0) }
val pagerState = rememberPagerState(pageCount = { 3 })
LaunchedEffect(selectedTabIndex) {
pagerState.animateScrollToPage(selectedTabIndex)
}
LaunchedEffect(pagerState.currentPage) {
selectedTabIndex = pagerState.currentPage
}
Column {
TabRow(selectedTabIndex = selectedTabIndex) {
listOf("Tab 1", "Tab 2", "Tab 3").forEachIndexed { index, title ->
Tab(
selected = index == selectedTabIndex,
onClick = { selectedTabIndex = index },
text = { Text(title) }
)
}
}
HorizontalPager(state = pagerState) { page ->
Text("Page $page content", modifier = Modifier.padding(16.dp))
}
}
}
Custom Indicator with TabIndicatorOffset
Create custom tab indicators with rounded corners:
@Composable
fun CustomIndicatorTabRow() {
var selectedTabIndex by remember { mutableIntStateOf(0) }
val tabs = listOf("Trending", "Following", "Saved")
Column {
TabRow(
selectedTabIndex = selectedTabIndex,
indicator = { tabPositions ->
Box(
modifier = Modifier
.tabIndicatorOffset(tabPositions[selectedTabIndex])
.height(4.dp)
.background(
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(topStart = 2.dp, topEnd = 2.dp)
)
)
}
) {
tabs.forEachIndexed { index, title ->
Tab(
selected = index == selectedTabIndex,
onClick = { selectedTabIndex = index },
text = { Text(title) }
)
}
}
}
}
Key Takeaways
- TabRow for fixed tabs that fit the screen
- ScrollableTabRow when tabs exceed available width
- HorizontalPager for swipeable content synchronized with tab selection
-
Custom indicators with
tabIndicatorOffsetfor branded UI - LaunchedEffect to keep tab index and pager state synchronized
Build responsive, touch-friendly tab layouts with these patterns!
8 Android app templates: Gumroad
Top comments (0)