Tutorial
Controlling and Customising Pull-to-Refresh in Android TWAs
September 30, 2026 · 6 min read
When packaging a Progressive Web App as an Android application using a Trusted Web Activity, maintaining user immersion is paramount. One behaviour that instantly exposes an app as a wrapped website is the default pull-to-refresh animation provided by the underlying browser engine. In a standard web browser, swiping downwards at the top of a page triggers a reload of the entire document. However, in a professional, native-feeling application, this behaviour is often undesirable, disruptive, and prone to breaking asynchronous client-side states.
The Problem with Browser-Level Refresh
Trusted Web Activities run on top of Chrome Custom Tabs. By default, Chrome handles overscroll at the top of a page by displaying a circular progress spinner that, once fully extended, forces a hard reload of the active URL. This presents several technical and user experience challenges for modern single-page applications.
First, complete document reloads wipe out client-side memory, temporary states, and active navigation histories. If a user is half-way through filling a form or has applied complex filters to a data table, an accidental downward swipe will erase their progress. Second, native apps rarely reload the entire screen shell; instead, they retrieve updated data asynchronously in the background and update the interface seamlessly. Relying on the default browser overscroll mechanism makes your application look unpolished and perform poorly on slower mobile networks.
Disabling Default Pull-to-Refresh with CSS
The first step in taking control of your application layout is to disable the browser engine default overscroll gestures. Fortunately, this does not require writing complex Android Java or Kotlin code. You can manage this entirely within your web codebase using the CSS overscroll-behavior property.
The overscroll-behavior CSS property tells the browser what to do when the boundary of a scrolling area is reached. By applying this property to your primary layout containers, you can suppress the browser-native pull-to-refresh gesture.
To block pull-to-refresh across your entire application, apply the property directly to the html and body elements:
html, body { overscroll-behavior-y: contain; }
Using the contain value prevents overscroll actions, such as page reloads or rubber-banding effects, from being passed up to the browser window, whilst preserving the natural scroll physics within nested scrollable areas. If you wish to disable all overscroll effects completely, you can use the none value instead:
html, body { overscroll-behavior-y: none; }
Designing a Custom Web-Based Refresh Gesture
Once the default browser action is disabled, you can implement a custom, native-feeling pull-to-refresh mechanism. This allows you to fetch data asynchronously via JavaScript and update your UI state without a disruptive page reload.
To build a custom pull-to-refresh component, you must listen for touch events at the top of your main scrolling container. The primary events required are touchstart, touchmove, and touchend.
When a touchstart event is detected, record the initial vertical coordinate of the user touch pointer. As the user performs a touchmove, calculate the distance scrolled downward. If the scroll container is at its top position (scrollTop is zero) and the downward distance is positive, you can programmatically translate your custom reload spinner element down the screen.
When the user releases their finger, triggering a touchend event, verify if the swipe distance exceeded a specified threshold. If the threshold is met, activate your data refresh function. If the threshold is not met, smoothly animate the custom spinner back to its hidden position using a CSS transition.
Once your asynchronous API calls resolve and your application state updates, animate the custom spinner back to its starting, invisible position to complete the loop.
Gestures and Expected Behaviours
Different gestures inside an Android app have specific expected outcomes. The table below outlines how standard gestures behave by default and how they should be handled in a polished Trusted Web Activity:
| Gesture | Default Browser Action | Native App Behaviour | Technical Implementation |
|---|---|---|---|
| Swipe down at top of page | Full document reload | Asynchronous data update | Set overscroll-behavior-y to contain and use touch events |
| Swipe down inside nested div | Scrolls container up | Scrolls container up without reload | Isolate scroll container with overscroll-behavior-y contain |
| Swipe past horizontal bounds | History navigation (back/forward) | No navigation or custom swiping | Set overscroll-behavior-x to none |
Handling Nested Scroll Containers
In complex applications, you may have multiple scrollable elements, such as side drawers, modal windows, or horizontal product carousels. If you only apply overscroll-behavior to the html or body tags, a user swiping within a modal might still inadvertently trigger browser-level actions if they reach the top of that specific container.
To prevent this, you should systematically apply overscroll-behavior to any container in your application that features scroll overflow. For example, if you have a scrollable side navigation panel, ensure you define its scroll containment rules separately:
.sidebar { overflow-y: auto; overscroll-behavior-y: contain; }
This ensures that scroll chaining stops at the boundary of the modal or sidebar and does not bubble up to the primary page body, eliminating unexpected document reloads regardless of where the user interacts with the viewport.
Testing the Experience in the TWA
To ensure your custom pull-to-refresh system functions perfectly, you must test it within the actual Trusted Web Activity environment, not just in a standard mobile browser. Mobile Chrome running standalone behaves slightly differently than a Chrome Custom Tab executing inside your signed APK container.
Build and install a debug APK of your application. Launch the application on an Android device or emulator, navigate to the top of your views, and aggressively pull downward. Verify that the standard grey-and-blue browser spinner does not appear, and that your custom indicator activates smoothly without dropping frames. Pay close attention to scroll physics and input lag during rapid swipes, adjusting your JavaScript touch event listeners to run passively to keep rendering thread performance optimal.
Ready to ship your Android app?
Paste your PWA URL, get a signed APK and a Google Play ready AAB in minutes.
Build my app