If you’ve ever tried implementing scroll in a Telegram MiniApp using the SDK, you probably noticed that the WebView inside Telegram behaves nothing like a regular browser. At first, I thought: “Well, overflow-y: auto and done!” — and I was sure scrolling would work just like in a typical React app.
But as soon as the app opened inside Telegram, strange things started happening: jittering scroll, bounce on iOS, stutters on Android, and sometimes the MiniApp would even close with a swipe down.
That’s when I realized one key thing: a Telegram MiniApp runs inside a WebView, and if you don’t take control over swipes and scrolling via the SDK, the interface will be chaotic and unpredictable.
So our goal is clear: make scrolling stable, smooth, and fully controlled — without losing control over the app’s navigation.
In this article, I use the Tailwind 4 CSS framework in the examples. If you’re not familiar with its classes, I recommend taking a short intro course to learn the basics.
In the following steps, I’ll show in detail how I organized the root wrapper, properly initialized the Telegram SDK, and created a reliable foundation for predictable scrolling inside React.
Basic root wrapper structure
BrowserRouter
└── Root div (swipe-y-off, overflow-hidden, max-w-[440px], min-h-screen, mx-auto)
├── SDK initialization (WebApp.ready(), WebApp.expand(), WebApp.disableVerticalSwipes())
└── AppRouter
├── Page 1
│ └── Scroll container (swipe-y-on, overflow-y-auto, height: 100vh)
│ ├── Main content (h-full)
│ └── 1px buffer
├── Page 2
│ └── Scroll container (swipe-y-on, overflow-y-auto, height: 100vh)
│ ├── Main content (h-full)
│ └── 1px buffer
└── Page N
└── Scroll container (swipe-y-on, overflow-y-auto, height: 100vh)
├── Main content (h-full)
└── 1px buffer
Required CSS rules for proper vertical swipes and scroll in Telegram MiniApp
Add this CSS to your global styles file so these classes are available throughout the app:
.swipe-y-off * {
touch-action: none; /* block all vertical swipes */
}
.swipe-y-off .swipe-y-on,
.swipe-y-off .swipe-y-on * {
overscroll-behavior: contain; /* prevent scroll chaining */
-webkit-overflow-scrolling: touch; /* smooth scrolling on iOS */
touch-action: pan-y !important; /* allow vertical swipe */
}
Rules breakdown
touch-action: none on .swipe-y-off *
Completely blocks all vertical swipes in child elements.
This is critical for scroll control inside Telegram MiniApps so system gestures don’t break scrolling.
overscroll-behavior: contain on .swipe-y-on
Prevents scroll from propagating outward.
For example, when the user scrolls to the bottom of a page, the WebView won’t start pulling the parent container.
-webkit-overflow-scrolling: touch
Adds smooth scrolling on iOS.
Without it, scrolling can feel jumpy — especially during fast swipes.
touch-action: pan-y !important on .swipe-y-on
Allows vertical swiping only where we explicitly enable it.
All other areas remain non-scrollable.
Base app provider and Telegram SDK initialization
Every proper MiniApp starts with a root wrapper and Telegram SDK setup. Honestly, this is where I personally doubted whether scroll would ever be fully stable. But let’s break it down step by step.
What the base provider does
In practice, the root component handles several tasks at once:
Tell Telegram the app is ready.
This is done via WebApp.ready(). Without it, the SDK may behave unpredictably, and scrolling inside the MiniApp will be chaotic.
Expand the app to full screen.WebApp.expand() ensures all content fits properly and scroll behaves predictably.
Disable Telegram system vertical swipes.
Without WebApp.disableVerticalSwipes(), any vertical movement could close the MiniApp — along with all our carefully built scrolling.
But there’s one nuance to remember: React doesn’t like direct state updates inside useEffect. So I wrap setReady(true) in a microtask using queueMicrotask. It’s a small detail, but without it you may encounter bugs or strange warnings.
Example base provider code
Here’s the full AppProvider component:
import { useState, useEffect } from "react";
import WebApp from "@twa-dev/sdk";
import { BrowserRouter } from "react-router-dom";
import { AppRouter } from "./Routes";
export function AppProvider() {
const [ready, setReady] = useState(false);
useEffect(() => {
WebApp.ready(); // Telegram: "ready"
WebApp.expand(); // expand to fullscreen
WebApp.disableVerticalSwipes(); // disable system swipes
// Microtask ensures React updates state correctly
queueMicrotask(() => setReady(true));
}, []);
// Render nothing until SDK is ready
if (!ready) return null;
return (
<BrowserRouter>
<div className="swipe-y-off overflow-hidden max-w-[440px] min-h-screen mx-auto">
<AppRouter /> {/* Connect your MiniApp routes here */}
</div>
</BrowserRouter>
);
}
Explanation of key details
if (!ready) return null acts as a small “guardian.”
Until the SDK is ready, the user sees no content. In practice, you can place a spinner or splash screen here.
The root div with class swipe-y-off is the foundation for scroll control in a Telegram MiniApp. It blocks system swipes so we can fully control scrolling inside our containers.
CSS classes overflow-hidden, max-w-[440px], min-h-screen, mx-auto ensure:
- Full-screen container behavior
- Mobile-friendly width limits
- Clean and predictable UI layout
Root wrapper CSS classes: the foundation of stable scroll
After setting up the base provider and initializing the SDK, it’s time to understand how CSS helps manage scrolling in Telegram MiniApps.
At this stage I realized: without the right wrapper, any scroll becomes chaotic. Telegram’s WebView tries to interfere with scrolling, pulls content around, and the interface starts “jumping.” These key classes become both our shield and foundation.
swipe-y-off — block all vertical swipes
This class is the main hero. Why?
- It completely disables Telegram system vertical swipes for all child elements
- Any container inside becomes non-scrollable by default
- We gain full control over scroll inside our own elements
Here’s a small lifehack: when you need scroll inside a container, you later apply swipe-y-on — more on that next.
overflow-hidden — isolate scroll from Telegram
Without this property, Telegram WebView tries to apply its own scroll physics. Then scrolling starts jittering.
overflow-hidden:
- Disables system scroll
- Makes all internal scroll containers independent
- Gives us full control when combined with
swipe-y-on
Size limits: max-w-[440px] and min-h-screen
Simple but important:
max-w-[440px]limits maximum app width — especially relevant for Telegram mobile viewmin-h-screenensures minimum height equals screen height
This guarantees scroll containers always fill the screen, even with little content.
Together, they provide stable scroll and clean visual layout.
Centering: mx-auto
A simple but visually important detail: horizontally centers the container on wider screens.
Combined with other classes, it creates a neat and predictable interface.
This entire combination forms the foundation for stable scroll in Telegram MiniApps — on top of which we build pages with swipe-y-on and a 1px micro-buffer.
Pages and scroll containers: enabling scroll where needed
So now we have a base wrapper with system swipes blocked and a foundation for predictable scroll. But if we leave it like this, scroll simply won’t work on specific pages.
The next step is creating separate scroll containers for each page where scrolling is actually needed. This prevents Telegram from interfering and allows users to scroll smoothly.
Example page with scroll container
export default function Page() {
return (
<div
className="swipe-y-on overflow-y-auto"
style={{ height: "100vh" }}
>
<div className="h-full flex flex-col">
<h1>Page title</h1>
<p>Main scrollable content goes here...</p>
<p>Let’s add more content to demonstrate scrolling.</p>
</div>
{/* 1px buffer stabilizes scroll */}
<div className="h-[1px]" />
</div>
);
}
Why this matters
swipe-y-onenables scroll only for the required container- Height
100vhfills the screen so scroll works correctly even with little content - The 1px micro-buffer stabilizes scroll physics and makes it feel almost native
With this approach, every page becomes predictable:
the interface is smooth, Telegram stops interfering, and users get comfortable scrolling.
You might ask: why all this complexity with swipe-y-off and swipe-y-on classes?
The answer is simple. In a real project you’ll have absolutely positioned blocks (header, bottom navigation, etc.), as well as spacing via padding or margins. Without this approach, these elements constantly trigger WebView movement during swipes.
P.S.
If you carefully follow all recommendations, correctly structure containers and wrappers, your Telegram MiniApp scroll will work exactly as intended.
Be attentive, check nesting carefully, and your interface will become smooth and predictable.
Wishing you success and inspiration in building new projects — may every MiniApp delight its users!