{% setvar book_path %}/reference/androidx/_book.yaml{% endsetvar %} {% include "_shared/_reference-head-tags.html" %}

TabRowKt

public final class TabRowKt


Summary

Public methods

static final void
@ExperimentalMaterial3Api
@Composable
PrimaryScrollableTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull ScrollState scrollState,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @NonNull Dp edgePadding,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Scrollable Primary tabs

static final void
@ExperimentalMaterial3Api
@Composable
PrimaryTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @Composable @ExtensionFunctionType @NonNull Function1<@NonNull TabIndicatorScopeUnit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Fixed Primary tabs

static final void
@Composable
ScrollableTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @NonNull Dp edgePadding,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design tabs

static final void
@ExperimentalMaterial3Api
@Composable
SecondaryScrollableTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull ScrollState scrollState,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @NonNull Dp edgePadding,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Scrollable Secondary tabs

static final void
@ExperimentalMaterial3Api
@Composable
SecondaryTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @Composable @ExtensionFunctionType @NonNull Function1<@NonNull TabIndicatorScopeUnit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Fixed Secondary tabs

static final void
@Composable
TabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design tabs

Public methods

PrimaryScrollableTabRow

@ExperimentalMaterial3Api
@Composable
public static final void PrimaryScrollableTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull ScrollState scrollState,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @NonNull Dp edgePadding,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Scrollable Primary tabs

Primary tabs are placed at the top of the content pane under a top app bar. They display the main content destinations. When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use longer text labels and a larger number of tabs. They are best used for browsing on touch interfaces.

A scrollable tab row contains a row of Tabs, and displays an indicator underneath the currently selected tab. A scrollable tab row places its tabs offset from the starting edge, and allows scrolling to tabs that are placed off screen. For a fixed tab row that does not allow scrolling, and evenly places its tabs, see PrimaryTabRow.

Parameters
int selectedTabIndex

the index of the currently selected tab

@NonNull Modifier modifier

the Modifier to be applied to this tab row

@NonNull ScrollState scrollState

the ScrollState of this tab row

@NonNull Color containerColor

the color used for the background of this tab row. Use Color.Transparent to have no color.

@NonNull Color contentColor

the preferred color for content inside this tab row. Defaults to either the matching content color for containerColor, or to the current LocalContentColor if containerColor is not a color from the theme.

@NonNull Dp edgePadding

the padding between the starting and ending edge of the scrollable tab row, and the tabs inside the row. This padding helps inform the user that this tab row can be scrolled, unlike a TabRow.

@Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator

the indicator that represents which tab is currently selected. By default this will be a TabRowDefaults.PrimaryIndicator, using a TabRowDefaults.tabIndicatorOffset modifier to animate its position.

@Composable @NonNull Function0<Unit> divider

the divider displayed at the bottom of the tab row. This provides a layer of separation between the tab row and the content displayed underneath.

@Composable @NonNull Function0<Unit> tabs

the tabs inside this tab row. Typically this will be multiple Tabs. Each element inside this lambda will be measured and placed evenly across the row, each taking up equal space.

PrimaryTabRow

@ExperimentalMaterial3Api
@Composable
public static final void PrimaryTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @Composable @ExtensionFunctionType @NonNull Function1<@NonNull TabIndicatorScopeUnit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Fixed Primary tabs

Primary tabs are placed at the top of the content pane under a top app bar. They display the main content destinations. Fixed tabs display all tabs in a set simultaneously. They are best for switching between related content quickly, such as between transportation methods in a map. To navigate between fixed tabs, tap an individual tab, or swipe left or right in the content area.

A TabRow contains a row of Tabs, and displays an indicator underneath the currently selected tab. A TabRow places its tabs evenly spaced along the entire row, with each tab taking up an equal amount of space. See PrimaryScrollableTabRow for a tab row that does not enforce equal size, and allows scrolling to tabs that do not fit on screen.

A simple example with text tabs looks like:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
Column {
    PrimaryTabRow(selectedTabIndex = state) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = state == index,
                onClick = { state = index },
                text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Primary tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}

You can also provide your own custom tab, such as:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")
Column {
    SecondaryTabRow(selectedTabIndex = state) {
        titles.forEachIndexed { index, title ->
            FancyTab(
                title = title,
                onClick = { state = index },
                selected = (index == state)
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Fancy tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}

Where the custom tab itself could look like:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

Tab(selected, onClick) {
    Column(
        Modifier
            .padding(10.dp)
            .height(50.dp)
            .fillMaxWidth(),
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        Box(
            Modifier
                .size(10.dp)
                .align(Alignment.CenterHorizontally)
                .background(
                    color = if (selected) MaterialTheme.colorScheme.primary
                    else MaterialTheme.colorScheme.background
                )
        )
        Text(
            text = title,
            style = MaterialTheme.typography.bodyLarge,
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
    }
}

As well as customizing the tab, you can also provide a custom indicator, to customize the indicator displayed for a tab. indicator will be placed to fill the entire TabRow, so it should internally take care of sizing and positioning the indicator to match changes to selectedTabIndex.

For example, given an indicator that draws a rounded rectangle near the edges of the Tab:

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Tab
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp

// Draws a rounded rectangular with border around the Tab, with a 5.dp padding from the edges
// Color is passed in as a parameter [color]
Box(
    modifier
        .padding(5.dp)
        .fillMaxSize()
        .border(BorderStroke(2.dp, color), RoundedCornerShape(5.dp))
)

We can reuse TabRowDefaults.tabIndicatorOffset and just provide this indicator, as we aren't changing how the size and position of the indicator changes between tabs:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")

Column {
    SecondaryTabRow(
        selectedTabIndex = state,
        indicator = {
            FancyIndicator(
                MaterialTheme.colorScheme.primary,
                Modifier.tabIndicatorOffset(state)
            )
        }
    ) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = state == index,
                onClick = { state = index },
                text = { Text(title) }
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Fancy indicator tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}

You may also want to use a custom transition, to allow you to dynamically change the appearance of the indicator as it animates between tabs, such as changing its color or size. indicator is stacked on top of the entire TabRow, so you just need to provide a custom transition that animates the offset of the indicator from the start of the TabRow. For example, take the following example that uses a transition to animate the offset, width, and color of the same FancyIndicator from before, also adding a physics based 'spring' effect to the indicator in the direction of motion:

import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset

val colors = listOf(
    MaterialTheme.colorScheme.primary,
    MaterialTheme.colorScheme.secondary,
    MaterialTheme.colorScheme.tertiary,
)
val transition = updateTransition(selectedTabIndex, label = "")
val indicatorStart by transition.animateDp(
    transitionSpec = {
        // Handle directionality here, if we are moving to the right, we
        // want the right side of the indicator to move faster, if we are
        // moving to the left, we want the left side to move faster.
        if (initialState < targetState) {
            spring(dampingRatio = 1f, stiffness = 50f)
        } else {
            spring(dampingRatio = 1f, stiffness = 1000f)
        }
    }, label = "fancy_indicator"
) {
    tabPositions[it].left
}

val indicatorEnd by transition.animateDp(
    transitionSpec = {
        // Handle directionality here, if we are moving to the right, we
        // want the right side of the indicator to move faster, if we are
        // moving to the left, we want the left side to move faster.
        if (initialState < targetState) {
            spring(dampingRatio = 1f, stiffness = 1000f)
        } else {
            spring(dampingRatio = 1f, stiffness = 50f)
        }
    }, label = "indicator_position"
) {
    tabPositions[it].right
}

val indicatorColor by transition.animateColor(label = "indicator_color") {
    colors[it % colors.size]
}

FancyIndicator(
    // Pass the current color to the indicator
    indicatorColor,
    modifier = Modifier
        // Fill up the entire TabRow, and place the indicator at the start
        .fillMaxSize()
        .wrapContentSize(align = Alignment.BottomStart)
        // Apply an offset from the start to correctly position the indicator around the tab
        .offset { IntOffset(x = indicatorStart.roundToPx(), y = 0) }
        // Make the width of the indicator follow the animated width as we move between tabs
        .width(indicatorEnd - indicatorStart)
)

We can now just pass this indicator directly to TabRow:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")

Column {
    SecondaryTabRow(
        selectedTabIndex = state,
        indicator = { FancyAnimatedIndicatorWithModifier(state) }
    ) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = state == index,
                onClick = { state = index },
                text = { Text(title) }
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Fancy transition tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}
Parameters
int selectedTabIndex

the index of the currently selected tab

@NonNull Modifier modifier

the Modifier to be applied to this tab row

@NonNull Color containerColor

the color used for the background of this tab row. Use Color.Transparent to have no color.

@NonNull Color contentColor

the preferred color for content inside this tab row. Defaults to either the matching content color for containerColor, or to the current LocalContentColor if containerColor is not a color from the theme.

@Composable @ExtensionFunctionType @NonNull Function1<@NonNull TabIndicatorScopeUnit> indicator

the indicator that represents which tab is currently selected. By default this will be a TabRowDefaults.PrimaryIndicator, using a TabRowDefaults.tabIndicatorOffset modifier to animate its position.

@Composable @NonNull Function0<Unit> divider

the divider displayed at the bottom of the tab row. This provides a layer of separation between the tab row and the content displayed underneath.

@Composable @NonNull Function0<Unit> tabs

the tabs inside this tab row. Typically this will be multiple Tabs. Each element inside this lambda will be measured and placed evenly across the row, each taking up equal space.

ScrollableTabRow

@Composable
public static final void ScrollableTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @NonNull Dp edgePadding,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design tabs

Material Design scrollable tabs.

For primary indicator tabs, use PrimaryScrollableTabRow. For secondary indicator tabs, use SecondaryScrollableTabRow.

When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use longer text labels and a larger number of tabs. They are best used for browsing on touch interfaces.

A ScrollableTabRow contains a row of Tabs, and displays an indicator underneath the currently selected tab. A ScrollableTabRow places its tabs offset from the starting edge, and allows scrolling to tabs that are placed off screen. For a fixed tab row that does not allow scrolling, and evenly places its tabs, see TabRow.

Parameters
int selectedTabIndex

the index of the currently selected tab

@NonNull Modifier modifier

the Modifier to be applied to this tab row

@NonNull Color containerColor

the color used for the background of this tab row. Use Color.Transparent to have no color.

@NonNull Color contentColor

the preferred color for content inside this tab row. Defaults to either the matching content color for containerColor, or to the current LocalContentColor if containerColor is not a color from the theme.

@NonNull Dp edgePadding

the padding between the starting and ending edge of the scrollable tab row, and the tabs inside the row. This padding helps inform the user that this tab row can be scrolled, unlike a TabRow.

@Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator

the indicator that represents which tab is currently selected. By default this will be a TabRowDefaults.SecondaryIndicator, using a TabRowDefaults.tabIndicatorOffset modifier to animate its position. Note that this indicator will be forced to fill up the entire tab row, so you should use TabRowDefaults.tabIndicatorOffset or similar to animate the actual drawn indicator inside this space, and provide an offset from the start.

@Composable @NonNull Function0<Unit> divider

the divider displayed at the bottom of the tab row. This provides a layer of separation between the tab row and the content displayed underneath.

@Composable @NonNull Function0<Unit> tabs

the tabs inside this tab row. Typically this will be multiple Tabs. Each element inside this lambda will be measured and placed evenly across the row, each taking up equal space.

SecondaryScrollableTabRow

@ExperimentalMaterial3Api
@Composable
public static final void SecondaryScrollableTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull ScrollState scrollState,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @NonNull Dp edgePadding,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Scrollable Secondary tabs

Material Design scrollable tabs.

Secondary tabs are used within a content area to further separate related content and establish hierarchy. When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use longer text labels and a larger number of tabs. They are best used for browsing on touch interfaces.

A scrollabel tab row contains a row of Tabs, and displays an indicator underneath the currently selected tab. A scrollable tab row places its tabs offset from the starting edge, and allows scrolling to tabs that are placed off screen. For a fixed tab row that does not allow scrolling, and evenly places its tabs, see SecondaryTabRow.

Parameters
int selectedTabIndex

the index of the currently selected tab

@NonNull Modifier modifier

the Modifier to be applied to this tab row

@NonNull ScrollState scrollState

the ScrollState of this tab row

@NonNull Color containerColor

the color used for the background of this tab row. Use Color.Transparent to have no color.

@NonNull Color contentColor

the preferred color for content inside this tab row. Defaults to either the matching content color for containerColor, or to the current LocalContentColor if containerColor is not a color from the theme.

@NonNull Dp edgePadding

the padding between the starting and ending edge of the scrollable tab row, and the tabs inside the row. This padding helps inform the user that this tab row can be scrolled, unlike a TabRow.

@Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator

the indicator that represents which tab is currently selected. By default this will be a TabRowDefaults.SecondaryIndicator, using a TabRowDefaults.tabIndicatorOffset modifier to animate its position. Note that this indicator will be forced to fill up the entire tab row, so you should use TabRowDefaults.tabIndicatorOffset or similar to animate the actual drawn indicator inside this space, and provide an offset from the start.

@Composable @NonNull Function0<Unit> divider

the divider displayed at the bottom of the tab row. This provides a layer of separation between the tab row and the content displayed underneath.

@Composable @NonNull Function0<Unit> tabs

the tabs inside this tab row. Typically this will be multiple Tabs. Each element inside this lambda will be measured and placed evenly across the row, each taking up equal space.

SecondaryTabRow

@ExperimentalMaterial3Api
@Composable
public static final void SecondaryTabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @Composable @ExtensionFunctionType @NonNull Function1<@NonNull TabIndicatorScopeUnit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design Fixed Secondary tabs

Secondary tabs are used within a content area to further separate related content and establish hierarchy. Fixed tabs display all tabs in a set simultaneously. To navigate between fixed tabs, tap an individual tab, or swipe left or right in the content area.

A TabRow contains a row of Tabs, and displays an indicator underneath the currently selected tab. A Fixed TabRow places its tabs evenly spaced along the entire row, with each tab taking up an equal amount of space. See SecondaryScrollableTabRow for a tab row that does not enforce equal size, and allows scrolling to tabs that do not fit on screen.

A simple example with text tabs looks like:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
Column {
    SecondaryTabRow(selectedTabIndex = state) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = state == index,
                onClick = { state = index },
                text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Secondary tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}
Parameters
int selectedTabIndex

the index of the currently selected tab

@NonNull Modifier modifier

the Modifier to be applied to this tab row

@NonNull Color containerColor

the color used for the background of this tab row. Use Color.Transparent to have no color.

@NonNull Color contentColor

the preferred color for content inside this tab row. Defaults to either the matching content color for containerColor, or to the current LocalContentColor if containerColor is not a color from the theme.

@Composable @ExtensionFunctionType @NonNull Function1<@NonNull TabIndicatorScopeUnit> indicator

the indicator that represents which tab is currently selected. By default this will be a TabRowDefaults.SecondaryIndicator, using a TabRowDefaults.tabIndicatorOffset modifier to animate its position. Note that this indicator will be forced to fill up the entire tab row, so you should use TabRowDefaults.tabIndicatorOffset or similar to animate the actual drawn indicator inside this space, and provide an offset from the start.

@Composable @NonNull Function0<Unit> divider

the divider displayed at the bottom of the tab row. This provides a layer of separation between the tab row and the content displayed underneath.

@Composable @NonNull Function0<Unit> tabs

the tabs inside this tab row. Typically this will be multiple Tabs. Each element inside this lambda will be measured and placed evenly across the row, each taking up equal space.

TabRow

@Composable
public static final void TabRow(
    int selectedTabIndex,
    @NonNull Modifier modifier,
    @NonNull Color containerColor,
    @NonNull Color contentColor,
    @Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator,
    @Composable @NonNull Function0<Unit> divider,
    @Composable @NonNull Function0<Unit> tabs
)

Material Design tabs

Material Design fixed tabs.

For primary indicator tabs, use PrimaryTabRow. For secondary indicator tabs, use SecondaryTabRow.

Fixed tabs display all tabs in a set simultaneously. They are best for switching between related content quickly, such as between transportation methods in a map. To navigate between fixed tabs, tap an individual tab, or swipe left or right in the content area.

A TabRow contains a row of Tabs, and displays an indicator underneath the currently selected tab. A TabRow places its tabs evenly spaced along the entire row, with each tab taking up an equal amount of space. See ScrollableTabRow for a tab row that does not enforce equal size, and allows scrolling to tabs that do not fit on screen.

A simple example with text tabs looks like:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
Column {
    PrimaryTabRow(selectedTabIndex = state) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = state == index,
                onClick = { state = index },
                text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Text tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}

You can also provide your own custom tab, such as:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")
Column {
    SecondaryTabRow(selectedTabIndex = state) {
        titles.forEachIndexed { index, title ->
            FancyTab(
                title = title,
                onClick = { state = index },
                selected = (index == state)
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Fancy tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}

Where the custom tab itself could look like:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

Tab(selected, onClick) {
    Column(
        Modifier
            .padding(10.dp)
            .height(50.dp)
            .fillMaxWidth(),
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        Box(
            Modifier
                .size(10.dp)
                .align(Alignment.CenterHorizontally)
                .background(
                    color = if (selected) MaterialTheme.colorScheme.primary
                    else MaterialTheme.colorScheme.background
                )
        )
        Text(
            text = title,
            style = MaterialTheme.typography.bodyLarge,
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
    }
}

As well as customizing the tab, you can also provide a custom indicator, to customize the indicator displayed for a tab. indicator will be placed to fill the entire TabRow, so it should internally take care of sizing and positioning the indicator to match changes to selectedTabIndex.

For example, given an indicator that draws a rounded rectangle near the edges of the Tab:

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Tab
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp

// Draws a rounded rectangular with border around the Tab, with a 5.dp padding from the edges
// Color is passed in as a parameter [color]
Box(
    modifier
        .padding(5.dp)
        .fillMaxSize()
        .border(BorderStroke(2.dp, color), RoundedCornerShape(5.dp))
)

We can reuse TabRowDefaults.tabIndicatorOffset and just provide this indicator, as we aren't changing how the size and position of the indicator changes between tabs:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")

Column {
    SecondaryTabRow(
        selectedTabIndex = state,
        indicator = {
            FancyIndicator(
                MaterialTheme.colorScheme.primary,
                Modifier.tabIndicatorOffset(state)
            )
        }
    ) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = state == index,
                onClick = { state = index },
                text = { Text(title) }
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Fancy indicator tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}

You may also want to use a custom transition, to allow you to dynamically change the appearance of the indicator as it animates between tabs, such as changing its color or size. indicator is stacked on top of the entire TabRow, so you just need to provide a custom transition that animates the offset of the indicator from the start of the TabRow. For example, take the following example that uses a transition to animate the offset, width, and color of the same FancyIndicator from before, also adding a physics based 'spring' effect to the indicator in the direction of motion:

import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset

val colors = listOf(
    MaterialTheme.colorScheme.primary,
    MaterialTheme.colorScheme.secondary,
    MaterialTheme.colorScheme.tertiary,
)
val transition = updateTransition(selectedTabIndex, label = "")
val indicatorStart by transition.animateDp(
    transitionSpec = {
        // Handle directionality here, if we are moving to the right, we
        // want the right side of the indicator to move faster, if we are
        // moving to the left, we want the left side to move faster.
        if (initialState < targetState) {
            spring(dampingRatio = 1f, stiffness = 50f)
        } else {
            spring(dampingRatio = 1f, stiffness = 1000f)
        }
    }, label = "fancy_indicator"
) {
    tabPositions[it].left
}

val indicatorEnd by transition.animateDp(
    transitionSpec = {
        // Handle directionality here, if we are moving to the right, we
        // want the right side of the indicator to move faster, if we are
        // moving to the left, we want the left side to move faster.
        if (initialState < targetState) {
            spring(dampingRatio = 1f, stiffness = 1000f)
        } else {
            spring(dampingRatio = 1f, stiffness = 50f)
        }
    }, label = "indicator_position"
) {
    tabPositions[it].right
}

val indicatorColor by transition.animateColor(label = "indicator_color") {
    colors[it % colors.size]
}

FancyIndicator(
    // Pass the current color to the indicator
    indicatorColor,
    modifier = Modifier
        // Fill up the entire TabRow, and place the indicator at the start
        .fillMaxSize()
        .wrapContentSize(align = Alignment.BottomStart)
        // Apply an offset from the start to correctly position the indicator around the tab
        .offset { IntOffset(x = indicatorStart.roundToPx(), y = 0) }
        // Make the width of the indicator follow the animated width as we move between tabs
        .width(indicatorEnd - indicatorStart)
)

We can now just pass this indicator directly to TabRow:

import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")

Column {
    SecondaryTabRow(
        selectedTabIndex = state,
        indicator = { FancyAnimatedIndicatorWithModifier(state) }
    ) {
        titles.forEachIndexed { index, title ->
            Tab(
                selected = state == index,
                onClick = { state = index },
                text = { Text(title) }
            )
        }
    }
    Text(
        modifier = Modifier.align(Alignment.CenterHorizontally),
        text = "Fancy transition tab ${state + 1} selected",
        style = MaterialTheme.typography.bodyLarge
    )
}
Parameters
int selectedTabIndex

the index of the currently selected tab

@NonNull Modifier modifier

the Modifier to be applied to this tab row

@NonNull Color containerColor

the color used for the background of this tab row. Use Color.Transparent to have no color.

@NonNull Color contentColor

the preferred color for content inside this tab row. Defaults to either the matching content color for containerColor, or to the current LocalContentColor if containerColor is not a color from the theme.

@Composable @NonNull Function1<@NonNull List<@NonNull TabPosition>, Unit> indicator

the indicator that represents which tab is currently selected. By default this will be a TabRowDefaults.SecondaryIndicator, using a TabRowDefaults.tabIndicatorOffset modifier to animate its position. Note that this indicator will be forced to fill up the entire tab row, so you should use TabRowDefaults.tabIndicatorOffset or similar to animate the actual drawn indicator inside this space, and provide an offset from the start.

@Composable @NonNull Function0<Unit> divider

the divider displayed at the bottom of the tab row. This provides a layer of separation between the tab row and the content displayed underneath.

@Composable @NonNull Function0<Unit> tabs

the tabs inside this tab row. Typically this will be multiple Tabs. Each element inside this lambda will be measured and placed evenly across the row, each taking up equal space.