Skip to content

Fix Horizontal Scroll Conflicts with Android Back Gesture in Compose

The Problem

I built a horizontal carousel in my Android app. Users loved swiping through images. But there was a problem: when they swiped near the left edge of the screen, the app went back instead of scrolling the carousel.

This wasn’t a bug in my code. It was a gesture conflict between my horizontal scroll and Android’s system back gesture. The system back gesture activates when users swipe from the screen edges, and my carousel was catching those same swipes.

The result? Frustrated users who accidentally navigated away when they just wanted to see the next image.

Why This Happens

Android reserves areas near the screen edges for system gestures. When you swipe from the left or right edge, Android interprets this as a back navigation gesture - not a scroll gesture for your app.

This makes sense for most apps. Users expect a consistent way to go back. But for horizontal scrolling content near the edges, this creates a usability problem.

The key insight is that Android provides APIs to handle this. You can either:

  1. Exclude your UI from system gestures entirely
  2. Respect system gesture zones by padding your content away from edges
  3. Define custom exclusion areas for specific components

Let me show you each approach.

Solution 1: Exclude System Gestures Entirely

The simplest solution is to tell Android “don’t use system gestures on this area.” Use Modifier.systemGestureExclusion():

HorizontalCarousel.kt
@Composable
fun HorizontalCarousel(
items: List<CarouselItem>
) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.systemGestureExclusion() // This tells Android: my gestures take priority
) {
items(items) { item ->
CarouselItem(item)
}
}
}

This works, but it has a trade-off. Users can no longer use the back gesture anywhere over your carousel. If your carousel fills the screen, users lose the ability to go back via gesture entirely.

Use this approach when:

  • Your horizontal scroll is the primary interaction
  • Users have an alternative way to navigate back (like a visible back button)
  • The scroll area is small enough that users can still gesture around it

Solution 2: Respect System Gesture Zones

A better approach for most cases is to pad your content away from the system gesture areas. First, you need to know where those areas are:

SafeHorizontalPager.kt
@Composable
fun SafeHorizontalPager(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val density = LocalDensity.current
val gestureExclusion = WindowInsets.systemGestureExclusion
// Get the left and right exclusion zones
val leftPadding = gestureExclusion.getLeft(density, 0)
val rightPadding = gestureExclusion.getRight(density, 0)
Box(
modifier = modifier
.padding(
start = leftPadding,
end = rightPadding
)
) {
content()
}
}

Now your horizontal pager respects the system gesture zones. Users can still use the back gesture from the edges, and they can scroll your content in the padded area.

Here’s how you’d use it with a HorizontalPager:

ImageGallery.kt
@Composable
fun ImageGallery(
images: List<Image>
) {
SafeHorizontalPager {
HorizontalPager(
pageCount = images.size,
modifier = Modifier.fillMaxSize()
) { page ->
AsyncImage(
model = images[page].url,
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}

This approach works well when:

  • You want to preserve system gesture functionality
  • Your content doesn’t need to extend to the absolute edges
  • You want a consistent UX that respects Android conventions

Solution 3: Custom Exclusion Rectangles

Sometimes you need fine-grained control. Maybe you want to exclude only a specific carousel, not the entire row. Or you want to exclude based on the component’s actual position on screen.

CustomExclusionExample.kt
@Composable
fun CustomExclusionExample() {
var exclusionRect by remember { mutableStateOf<Rect?>(null) }
Box(
modifier = Modifier
.onGloballyPositioned { coordinates ->
// Capture the component's position on screen
exclusionRect = coordinates.boundsInRoot()
}
.systemGestureExclusion {
// Only exclude the area this component actually occupies
exclusionRect?.toAndroidRect() ?: Rect()
}
) {
// Your gesture-sensitive content here
HorizontalCarousel(items = sampleItems)
}
}
// Extension function to convert Compose Rect to Android Rect
private fun Rect.toAndroidRect(): android.graphics.Rect {
return android.graphics.Rect(
left.toInt(),
top.toInt(),
right.toInt(),
bottom.toInt()
)
}

This gives you precise control. The exclusion zone is exactly where your component is, not more, not less.

Android 14+: Predictive Back

Starting with Android 14, Android supports predictive back gestures. This shows users a preview of where they’ll go when they swipe back. If you’re handling back gestures yourself, you should integrate with this system:

PredictiveBackHandler.kt
@Composable
fun PredictiveBackAwareScreen(
onBack: () -> Unit
) {
val predictiveBackHandler = rememberPredictiveBackHandler { progress ->
// progress provides the gesture completion percentage
// You can animate your UI based on this
try {
progress.collect { backEvent ->
// backEvent.progress: Float (0.0 to 1.0)
// backEvent.touchX, touchY: Float (gesture position)
// Use these to animate your custom back behavior
}
// Gesture completed - perform the back action
onBack()
} catch (e: CancellationException) {
// User cancelled the gesture - reset your animations
}
}
// Your content here
}

Predictive back makes gesture navigation feel more natural. Users see where they’re going before they commit to the action.

Testing on Real Devices

One important lesson I learned: test gesture handling on physical devices, not just emulators.

Emulators often have different gesture behavior than real devices. The sensitivity, timing, and even the gesture zones can vary. I’ve had gesture conflicts that only appeared on my Pixel but worked fine in the emulator.

Here’s my testing checklist:

  1. Test on at least one device with gesture navigation (not the old three-button nav)
  2. Test on different screen sizes - gesture zones scale with screen dimensions
  3. Test in both landscape and portrait orientations
  4. Verify the behavior on Android 14+ for predictive back

Choosing the Right Approach

Here’s my decision process:

SituationRecommended Approach
Small horizontal carousel with alternative back navigationsystemGestureExclusion() modifier
Full-screen horizontal pagerPadding with WindowInsets.systemGestureExclusion
Mixed content with specific gesture-sensitive areasCustom exclusion rectangles
Android 14+ app with custom back handlingPredictive back handler

Summary

In this post, I showed you how to resolve gesture conflicts between horizontal scrolling and Android’s system back gesture in Jetpack Compose.

The key solutions are:

  • Modifier.systemGestureExclusion() to completely exclude areas from system gestures
  • WindowInsets.systemGestureExclusion to pad content away from system gesture zones
  • Custom exclusion rectangles for fine-grained control
  • Predictive back handlers for Android 14+ integration

The right choice depends on your UX requirements. If users need system gestures, pad your content. If your app’s gestures are more important, exclude the areas. Test on real devices to catch issues emulators miss.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments