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:
- Exclude your UI from system gestures entirely
- Respect system gesture zones by padding your content away from edges
- 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():
@Composablefun 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:
@Composablefun 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:
@Composablefun 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.
@Composablefun 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 Rectprivate 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:
@Composablefun 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:
- Test on at least one device with gesture navigation (not the old three-button nav)
- Test on different screen sizes - gesture zones scale with screen dimensions
- Test in both landscape and portrait orientations
- Verify the behavior on Android 14+ for predictive back
Choosing the Right Approach
Here’s my decision process:
| Situation | Recommended Approach |
|---|---|
| Small horizontal carousel with alternative back navigation | systemGestureExclusion() modifier |
| Full-screen horizontal pager | Padding with WindowInsets.systemGestureExclusion |
| Mixed content with specific gesture-sensitive areas | Custom exclusion rectangles |
| Android 14+ app with custom back handling | Predictive 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 gesturesWindowInsets.systemGestureExclusionto 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