TopAppBar in Jetpack Compose: Setup, Scroll Behavior, and Actions

Quick answer: Put a Material 3 top app bar in Scaffold(topBar = { ... }), choose TopAppBar, CenterAlignedTopAppBar, MediumTopAppBar, or LargeTopAppBar according to the screen hierarchy, and pass innerPadding to the scrolling content. For collapsing behavior, create a TopAppBarScrollBehavior, attach its nestedScrollConnection to the Scaffold, and pass the same behavior to the app bar.

A top app bar gives users a stable place to understand where they are, navigate back, and reach the screen’s most important actions. In Compose, the app bar is a composable rather than a special activity-level widget, so you can keep it next to the Scaffold and make its state and events explicit.

This guide covers the Material 3 API, a complete Scaffold example, scroll behavior, actions, colors, accessibility, and the mistakes that make app bars feel inconsistent.

Choose the right top app bar

Material 3 provides four top app bar composables. They share the core slots but differ in title placement and vertical space:

ComposableUse it whenTitle placement
TopAppBarThe screen needs a compact title and a small number of actionsOne row, aligned with the icons
CenterAlignedTopAppBarThe title should be centered and the screen has one primary focusOne row, centered
MediumTopAppBarThe screen needs a little more title space and several actionsTitle sits below the icon row
LargeTopAppBarThe title is a prominent part of the screen hierarchyMore vertical space, with the title below the icons

The official app-bar guide describes top app bars as containers for a title, navigation items, and key actions. Select the smallest variant that gives the title and actions enough room. A large bar on every screen makes a simple destination feel heavier than it is.

The basic TopAppBar inside Scaffold

Scaffold provides a slot for the top bar and gives its content lambda the PaddingValues needed to stay clear of that bar. Apply those values to the screen content instead of adding a second, guessed top padding.

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun DetailsScreen(
    onNavigateUp: () -> Unit,
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Details") },
                navigationIcon = {
                    IconButton(onClick = onNavigateUp) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = "Navigate up",
                        )
                    }
                },
            )
        },
    ) { innerPadding: PaddingValues ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
        ) {
            // Screen content goes here.
        }
    }
}

The navigation icon is optional. Include it when the destination has a clear back or up action; do not add a decorative arrow that does nothing. The Scaffold and Window Insets guide explains how this padding fits into an edge-to-edge screen.

Add navigation and action slots

The navigationIcon slot is on the leading side of the bar. The actions slot is a row on the trailing side for key actions such as search, share, or an overflow menu. Keep actions at the same level of importance as the current screen, not a complete collection of every possible command.

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable

@Composable
fun SearchableTopBar(
    onNavigateUp: () -> Unit,
    onSearch: () -> Unit,
    onMore: () -> Unit,
) {
    TopAppBar(
        title = { Text("Library") },
        navigationIcon = {
            IconButton(onClick = onNavigateUp) {
                Icon(
                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = "Navigate up",
                )
            }
        },
        actions = {
            IconButton(onClick = onSearch) {
                Icon(
                    imageVector = Icons.Filled.Search,
                    contentDescription = "Search library",
                )
            }
            IconButton(onClick = onMore) {
                Icon(
                    imageVector = Icons.Filled.MoreVert,
                    contentDescription = "More options",
                )
            }
        },
    )
}

This example assumes the Material icons dependency. The visible icon is not an accessible name by itself, so every standalone icon button needs a localized contentDescription. When an icon sits beside a visible text label, make it decorative with contentDescription = null instead of announcing the same information twice.

Make a medium or large bar collapse on scroll

A scroll behavior connects the app bar to the nested-scroll events produced by the content below it. The three standard behaviors have different expectations:

  • pinnedScrollBehavior() keeps the bar in place while content scrolls.
  • enterAlwaysScrollBehavior() hides the bar as the user scrolls up and brings it back as soon as the user scrolls down.
  • exitUntilCollapsedScrollBehavior() collapses the bar while scrolling up and keeps it collapsed until the content reaches the top again.

The Android quick guide for displaying a top app bar uses enterAlwaysScrollBehavior() with a MediumTopAppBar. The important detail is that the same behavior is used in both places: Scaffold receives its nestedScrollConnection, and the app bar receives the behavior itself.

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp

@Composable
fun ReadingListScreen(
    titles: List<String>,
) {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
        state = rememberTopAppBarState(),
    )

    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            MediumTopAppBar(
                title = {
                    Text(
                        text = "Reading list",
                        maxLines = 1,
                    )
                },
                scrollBehavior = scrollBehavior,
            )
        },
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(
                top = innerPadding.calculateTopPadding(),
                bottom = innerPadding.calculateBottomPadding(),
                start = 16.dp,
                end = 16.dp,
            ),
            verticalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            items(
                items = titles,
                key = { it },
            ) { title ->
                Text(
                    text = title,
                    style = MaterialTheme.typography.bodyLarge,
                    modifier = Modifier.padding(vertical = 12.dp),
                )
            }
        }
    }
}

The key = { it } line is appropriate only when each title is unique. If titles can repeat, use a real stable ID from your UI model. For simple screens, Modifier.padding(innerPadding) is enough; the expanded contentPadding form above lets the list retain its own horizontal spacing while honoring the scaffold’s vertical insets.

rememberTopAppBarState() is Compose-owned UI state, so keeping it in the composable is appropriate. Application state—such as the list contents, loading state, and navigation events—should remain in a ViewModel or screen state holder. See State Hoisting in Jetpack Compose for the boundary between screen state and Compose-internal scroll state.

Pick a behavior by screen intent

Pinned bars for task-focused screens

Use pinnedScrollBehavior() when the title or action must remain visible, such as a messaging screen with a persistent compose action or a detail screen where the back button should never disappear. You can also omit scrollBehavior entirely when the bar should not react to scrolling; a pinned behavior is useful when you want the app bar’s state and color transitions to remain connected to nested scrolling.

Enter-always bars for content browsing

Use enterAlwaysScrollBehavior() for a feed or reading list where users benefit from reclaiming vertical space while scrolling, but expect the bar to return immediately when they reverse direction. This is often a good default for a medium bar with a short title.

Exit-until-collapsed bars for prominent titles

Use exitUntilCollapsedScrollBehavior() for a large or medium title that should collapse as the user explores content and return when they scroll all the way back to the top. It preserves the screen identity during the initial view without permanently consuming the viewport.

Do not select a behavior only because the animation looks impressive. Check how often users need the navigation and actions, how much content the title occupies, and whether the bar’s movement distracts from the task.

Customize colors without breaking the theme

Start with Material 3 defaults so the app bar follows your light, dark, and dynamic color schemes. When a screen needs a deliberate variant, use TopAppBarDefaults.topAppBarColors and pair each container role with its matching content role.

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable

@Composable
fun ThemedTopBar() {
    TopAppBar(
        title = { Text("Projects") },
        colors = TopAppBarDefaults.topAppBarColors(
            containerColor = MaterialTheme.colorScheme.primaryContainer,
            titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
            navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
            actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
        ),
    )
}

The MaterialTheme guide covers semantic color roles and typography. Avoid hard-coded white icons or text on a custom container; a color that passes contrast in one palette can fail in dark or dynamic color. If the bar sits on a changing background, also verify its scrolled and collapsed colors in both light and dark previews.

Handle long titles and action overflow

Titles should describe the current destination, not repeat the application name. For titles that can grow with user content, set maxLines = 1 and use TextOverflow.Ellipsis so actions do not get pushed off-screen.

import androidx.compose.ui.text.style.TextOverflow

TopAppBar(
    title = {
        Text(
            text = projectName,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
        )
    },
)

If a screen has more than two or three frequent actions, move secondary commands into an overflow menu. Keep the most common action visible and give the menu button a description such as “More project options,” not just “Menu.” Avoid putting a search field, a long title, and several icon buttons in the same compact bar; choose a larger variant or move search into the content area.

Accessibility and testing

Material app bars provide useful layout and interaction semantics, but your slots still need meaningful labels. Follow the Compose accessibility defaults and test the merged semantics tree used by Compose UI tests.

  • Give navigation and action icons a localized description that names the result.
  • Keep the title concise and meaningful; it is often the first context a screen reader announces.
  • Do not make a decorative icon focusable or clickable.
  • Preserve the platform touch-target guidance for IconButton; do not shrink it to fit extra actions.
  • Test the bar with large font sizes, right-to-left locales, TalkBack, and a hardware keyboard.

A basic Compose UI test can verify that the navigation action is discoverable and invokes its callback:

composeTestRule
    .onNodeWithContentDescription("Navigate up")
    .assertExists()
    .performClick()

For a collapsing bar, test behavior at the screen level: scroll the actual LazyColumn, assert that the title remains present, and verify that content starts below the scaffold’s padding. Avoid testing implementation details such as a particular pixel offset unless the offset itself is a product requirement.

Common integration mistakes

Forgetting the nested-scroll connection

Passing scrollBehavior to MediumTopAppBar without attaching Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) to the Scaffold leaves the bar disconnected from the list. The app bar renders, but it will not react to the content’s scroll events.

Dropping innerPadding

Content that ignores the Scaffold padding can start underneath the app bar or end behind a bottom bar. Apply the padding to the scroll container, as shown in the Scaffold guide, and avoid stacking a second arbitrary top inset.

Nesting multiple scroll containers

Use one primary scroll container for a screen. A LazyColumn inside a vertically scrolling Column makes scroll ownership ambiguous and can produce awkward nested-scroll behavior. Put the app bar connection on the scaffold and let the primary list consume the scroll.

Handling every action at the UI layer

The app bar should render state and emit events. Navigation, search, saving, and menu decisions belong in the screen state holder or ViewModel. This keeps the same top bar previewable and makes action tests deterministic.

Treating every title as a large title

Large bars are useful when the destination title is part of the visual hierarchy. They are not a requirement for every screen. A compact TopAppBar often gives a list or form more useful vertical space.

FAQ

What is the difference between TopAppBar and CenterAlignedTopAppBar?

Both are compact, single-row bars. TopAppBar places the title in the standard start-aligned position after the navigation slot, while CenterAlignedTopAppBar centers the title for a screen whose primary focus benefits from that alignment.

Which scroll behavior should I use with LazyColumn?

Use pinnedScrollBehavior() when the bar must stay visible, enterAlwaysScrollBehavior() when it should return as soon as the user reverses direction, and exitUntilCollapsedScrollBehavior() when a medium or large title should remain collapsed until the list reaches the top. Connect the behavior to the scaffold’s nested scroll modifier.

Can I use a top app bar without Scaffold?

Yes, but Scaffold is the convenient Material 3 screen shell because it provides the top-bar slot and content padding. If you place the bar in a custom layout, you own measurement, insets, and the relationship between the bar and content.

How many actions belong in the app bar?

Expose the actions users need frequently in the current context. Move secondary or infrequent commands into an overflow menu. The exact number depends on title length, localization, font scale, and device width, so validate the real screen rather than relying on a fixed count.

Should the app bar own navigation state?

No. Pass callbacks such as onNavigateUp, onSearch, or onMore into a stateless app-bar composable. The screen or ViewModel decides what those events do, following the same state-hoisting approach used by other Material components such as Compose buttons.

Summary

Use TopAppBar for compact screens, CenterAlignedTopAppBar for a centered title, MediumTopAppBar for a moderate hierarchy, and LargeTopAppBar for a prominent destination title. Place the bar in Scaffold, apply innerPadding to the content, and connect a TopAppBarScrollBehavior through nestedScroll when the bar should react to scrolling. Keep actions focused, labels localized, colors theme-aware, and application state outside the composable. That combination produces app bars that feel predictable on phones, tablets, dark themes, and accessibility services.