TV-Material Design Horizontal TabRow

Android
@Composable
fun TabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    containerColor: Color = TabRowDefaults.ContainerColor,
    contentColor: Color = TabRowDefaults.contentColor(),
    separator: @Composable () -> Unit = { TabRowDefaults.TabSeparator() },
    indicator: @Composable (tabPositions: List<DpRect>, doesTabRowHaveFocus: Boolean) -> Unit =
        @Composable { tabPositions, doesTabRowHaveFocus ->
            tabPositions.getOrNull(selectedTabIndex)?.let { currentTabPosition ->
                TabRowDefaults.PillIndicator(
                    currentTabPosition = currentTabPosition,
                    doesTabRowHaveFocus = doesTabRowHaveFocus,
                )
            }
        },
    tabs: @Composable TabRowScope.() -> Unit,
)

Parameters

selectedTabIndex the index of the currently selected tab
modifier the Modifier to be applied to this tab row
containerColor the color used for the background of this tab row
contentColor the primary color used in the tabs
separator use this composable to add a separator between the tabs
indicator used to indicate which tab is currently selected and/or focused. This lambda provides 2 values:
tabs a composable which will render all the tabs

Code Examples

OnClickNavigation

/** Tab changes onClick instead of onFocus */
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun OnClickNavigation() {
    val bgColors =
        listOf(
            Color(0x6a, 0x16, 0x16),
            Color(0x6a, 0x40, 0x16),
            Color(0x6a, 0x6a, 0x16),
            Color(0x40, 0x6a, 0x16),
        )
    var focusedTabIndex by remember { mutableStateOf(0) }
    var activeTabIndex by remember { mutableStateOf(focusedTabIndex) }
    Box(modifier = Modifier.fillMaxSize().background(bgColors[activeTabIndex])) {
        TabRow(
            selectedTabIndex = focusedTabIndex,
            indicator = { tabPositions, doesTabRowHaveFocus ->
                // FocusedTab's indicator
                TabRowDefaults.PillIndicator(
                    currentTabPosition = tabPositions[focusedTabIndex],
                    activeColor = Color.Blue.copy(alpha = 0.4f),
                    inactiveColor = Color.Transparent,
                    doesTabRowHaveFocus = doesTabRowHaveFocus,
                )
                // SelectedTab's indicator
                TabRowDefaults.PillIndicator(
                    currentTabPosition = tabPositions[activeTabIndex],
                    doesTabRowHaveFocus = doesTabRowHaveFocus,
                )
            },
            modifier = Modifier.focusRestorer(),
        ) {
            repeat(bgColors.size) {
                key(it) {
                    Tab(
                        selected = activeTabIndex == it,
                        onFocus = { focusedTabIndex = it },
                        onClick = {
                            focusedTabIndex = it
                            activeTabIndex = it
                        },
                    ) {
                        Text(
                            text = "Tab ${it + 1}",
                            fontSize = 12.sp,
                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
                        )
                    }
                }
            }
        }
    }
}

PillIndicatorTabRow

/** Tab row with a Pill indicator */
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PillIndicatorTabRow() {
    val tabs = listOf("Tab 1", "Tab 2", "Tab 3")
    var selectedTabIndex by remember { mutableStateOf(0) }
    TabRow(selectedTabIndex = selectedTabIndex, modifier = Modifier.focusRestorer()) {
        tabs.forEachIndexed { index, tab ->
            key(index) {
                Tab(selected = index == selectedTabIndex, onFocus = { selectedTabIndex = index }) {
                    Text(
                        text = tab,
                        fontSize = 12.sp,
                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
                    )
                }
            }
        }
    }
}

TabRowWithDebounce

/** Tab row with delay between tab changes */
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun TabRowWithDebounce() {
    val tabs = listOf("Tab 1", "Tab 2", "Tab 3")
    var selectedTabIndex by remember { mutableStateOf(0) }
    // This index will be used to show a panel
    var tabPanelIndex by remember { mutableStateOf(selectedTabIndex) }
    // Change the tab-panel only after some delay
    LaunchedEffect(selectedTabIndex) {
        delay(250.microseconds)
        tabPanelIndex = selectedTabIndex
    }
    TabRow(selectedTabIndex = selectedTabIndex, modifier = Modifier.focusRestorer()) {
        tabs.forEachIndexed { index, tab ->
            key(index) {
                Tab(selected = index == selectedTabIndex, onFocus = { selectedTabIndex = index }) {
                    Text(
                        text = tab,
                        fontSize = 12.sp,
                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
                    )
                }
            }
        }
    }
}

UnderlinedIndicatorTabRow

/** Tab row with an Underlined indicator */
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun UnderlinedIndicatorTabRow() {
    val tabs = listOf("Tab 1", "Tab 2", "Tab 3")
    var selectedTabIndex by remember { mutableStateOf(0) }
    TabRow(
        selectedTabIndex = selectedTabIndex,
        indicator = { tabPositions, doesTabRowHaveFocus ->
            TabRowDefaults.UnderlinedIndicator(
                currentTabPosition = tabPositions[selectedTabIndex],
                doesTabRowHaveFocus = doesTabRowHaveFocus,
            )
        },
        modifier = Modifier.focusRestorer(),
    ) {
        tabs.forEachIndexed { index, tab ->
            key(index) {
                Tab(
                    selected = index == selectedTabIndex,
                    onFocus = { selectedTabIndex = index },
                    colors = TabDefaults.underlinedIndicatorTabColors(),
                ) {
                    Text(
                        text = tab,
                        fontSize = 12.sp,
                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
                    )
                }
            }
        }
    }
}