DEV Community

myougaTheAxo
myougaTheAxo

Posted on

TabLayout in Compose — ScrollableTabRow & HorizontalPager

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) }
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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") }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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) }
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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) }
                )
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 tabIndicatorOffset for 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)