Paging with Compose for Wear OS

Paging lets users swipe horizontally or vertically between distinct, full-screen pages on Wear OS devices. Common use cases include swiping between workout metrics and media controls in an exercise app, or stepping through multi-page flows.

In Compose for Wear OS Material 3, HorizontalPagerScaffold and VerticalPagerScaffold coordinate the pager layout, automatically positioning the page indicator and managing transitions with TimeText. Each page is wrapped in an AnimatedPage composable, which applies scaling, rounded corner morphing, and scrim effects as pages transition across the round display.

The following animation shows how HorizontalPagerScaffold and AnimatedPage scale and animate pages during horizontal swipes:

Scaffold hierarchy for paging

When designing paginated screen layouts in Material 3—whether swiping left or right (HorizontalPager) or up or down (VerticalPager)—use the following component hierarchy from outer container to inner content:

  1. AppScaffold: The outermost container at the root of your app (use only one per app). It anchors the global TimeText overlay so that the clock remains stationary at the top of the screen during page transitions and swipe-to-dismiss gestures.
  2. HorizontalPagerScaffold or VerticalPagerScaffold: Placed inside AppScaffold at the pager level. It coordinates transitions between TimeText and the HorizontalPageIndicator or VerticalPageIndicator.
  3. HorizontalPager or VerticalPager: The foundation pager container that manages swipe gestures, fling physics, and rotary input snapping using a shared PagerState.
  4. AnimatedPage: Placed inside the pager's page content lambda. It wraps each individual page to apply Material 3 transition animations (scaling and scrim effects) based on the page's offset in PagerState.
  5. ScreenScaffold: Placed inside AnimatedPage for each individual page. Because each page can contain its own vertically scrollable list (such as a TransformingLazyColumn) or its own EdgeButton, nesting ScreenScaffold inside each page ensures that vertical scroll indicators, edge buttons, and contentPadding belong to that specific page and animate smoothly with it.
AppScaffold (1 per app: anchors global TimeText)
 └── HorizontalPagerScaffold / VerticalPagerScaffold (manages PageIndicator)
      └── HorizontalPager / VerticalPager (manages PagerState & fling behavior)
           └── AnimatedPage (applies scaling & scrim transitions per page)
                └── ScreenScaffold (1 per page: ScrollIndicator & EdgeButton)
                     └── Page Content (Column or TransformingLazyColumn)

Implement a horizontal pager

To implement a horizontal pager in Material 3, nest HorizontalPagerScaffold, HorizontalPager, AnimatedPage, and ScreenScaffold inside your app's AppScaffold.

Use PagerScaffoldDefaults.snapWithSpringFlingBehavior for flingBehavior to apply Material 3 spring motion physics and responsive page snapping (HighSnapPositionalThreshold). By default, HorizontalPager disables rotary page scrolling (rotaryScrollableBehavior = null), allowing rotating crown or bezel input to scroll vertical lists (such as a TransformingLazyColumn) inside the active page.

The following sample demonstrates a complete HorizontalPagerScaffold setup:

@Composable
fun HorizontalPagerScaffoldSample(navigateBack: () -> Unit) {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 10 })

        HorizontalPagerScaffold(pagerState = pagerState) {
            HorizontalPager(
                state = pagerState,
                flingBehavior =
                    PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                        state = pagerState
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        Column(
                            modifier = Modifier.fillMaxSize(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                        ) {
                            Text(text = "Page #$page")
                            Spacer(modifier = Modifier.height(8.dp))
                            Text(text = "Swipe left and right")
                            if (page == 0) {
                                Spacer(modifier = Modifier.height(16.dp))
                                Button(onClick = navigateBack) { Text("Exit") }
                            }
                        }
                    }
                }
            }
        }
    }
}

If a page inside your horizontal pager contains a TransformingLazyColumn, create a separate rememberTransformingLazyColumnState() inside that page's scope and pass it to the inner ScreenScaffold and TransformingLazyColumn:

AppScaffold {
    val pagerState = rememberPagerState(pageCount = { 10 })

    HorizontalPagerScaffold(pagerState = pagerState) {
        HorizontalPager(
            state = pagerState,
            flingBehavior =
                PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                    state = pagerState
                ),
        ) { page ->
            AnimatedPage(pageIndex = page, pagerState = pagerState) {
                val columnState = rememberTransformingLazyColumnState()
                val transformationSpec = rememberTransformationSpec()

                ScreenScaffold(
                    scrollState = columnState,
                ) { contentPadding ->
                    TransformingLazyColumn(
                        state = columnState,
                        contentPadding = contentPadding,
                    ) {
                        item {
                            ListHeader(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .transformedHeight(this, transformationSpec)
                                    .minimumVerticalContentPadding(
                                        ListHeaderDefaults.minimumTopListContentPadding
                                    ),
                                transformation = SurfaceTransformation(transformationSpec),
                            ) {
                                Text(text = "Pager sample")
                            }
                        }
                        item {
                            Card(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .transformedHeight(this, transformationSpec)
                                    .minimumVerticalContentPadding(
                                        CardDefaults.minimumVerticalListContentPadding
                                    ),
                                transformation = SurfaceTransformation(transformationSpec),
                            ) {
                                if (page == 0) {
                                    Text(text = "Page #$page. Swipe right")
                                } else {
                                    Text(text = "Page #$page. Swipe left and right")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Implement a vertical pager

A vertical pager lets users swipe up and down between pages or step through them using the watch's rotating side button or bezel.

Unlike HorizontalPager, VerticalPager enables rotary page snapping by default (RotaryScrollableDefaults.snapBehavior(state)). Pass PagerScaffoldDefaults.snapWithSpringFlingBehavior to flingBehavior and wrap each page in AnimatedPage and ScreenScaffold:

@Composable
fun VerticalPagerScaffoldSample() {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 10 })

        VerticalPagerScaffold(pagerState = pagerState) {
            VerticalPager(
                state = pagerState,
                flingBehavior =
                    PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                        state = pagerState
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        Column(
                            modifier = Modifier.fillMaxSize(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                        ) {
                            Text(text = "Page #$page")
                            Spacer(modifier = Modifier.height(8.dp))
                            Text(text = "Swipe up and down")
                        }
                    }
                }
            }
        }
    }
}

Customize pager behavior

You can customize the snapping sensitivity of HorizontalPagerScaffold and VerticalPagerScaffold to match your app's interaction needs.

Adjust snap sensitivity for workouts

During workouts or high-motion activities where the user's gross motor control is limited, accidental touches or slight crown rotations can unintentionally switch pages. For these screens, configure your pager with low snap sensitivity:

  • Pass PagerDefaults.snapFlingBehavior with maxFlingPages = 0 and snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold so a deliberate drag across a larger portion of the screen is required to turn the page.
  • If you enable rotary page snapping, set snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity in RotaryScrollableDefaults.snapBehavior to require more rotation before snapping to the next page.
@Composable
fun HorizontalPagerScaffoldWithLowSensitivitySample(navigateBack: () -> Unit) {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 3 })

        HorizontalPagerScaffold(pagerState = pagerState) {
            HorizontalPager(
                state = pagerState,
                flingBehavior =
                    PagerDefaults.snapFlingBehavior(
                        state = pagerState,
                        maxFlingPages = 0,
                        snapPositionalThreshold =
                            PagerScaffoldDefaults.LowSnapPositionalThreshold,
                    ),
                rotaryScrollableBehavior =
                    RotaryScrollableDefaults.snapBehavior(
                        pagerState = pagerState,
                        snapSensitivity =
                            RotaryScrollableDefaults.LowSnapSensitivity,
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        // Page content
                    }
                }
            }
        }
    }
}

For full sample implementations, see Pager.kt in the android/snippets repository and PagerScaffoldSample.kt in the AndroidX repository.