diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index 963961a744..d5a1f9dbe9 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -192,6 +192,11 @@
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) transparent;
}
+ /* App shell: lock the document to the viewport; each route owns its scroll container. */
+ html,
+ body {
+ @apply h-full overflow-hidden overscroll-none;
+ }
body {
@apply bg-background text-foreground;
}
diff --git a/apps/web/components/settings/settings-layout-client.tsx b/apps/web/components/settings/settings-layout-client.tsx
index e57c20a6d0..4e8ef689ec 100644
--- a/apps/web/components/settings/settings-layout-client.tsx
+++ b/apps/web/components/settings/settings-layout-client.tsx
@@ -116,7 +116,10 @@ function SettingsShell({
/>
{/* Scroll the content, not the topbar: min-h-0 lets this flex child
shrink below its content height so overflow-y-auto can take effect. */}
-
+
{children}
diff --git a/apps/web/e2e/tests/automations-settings.spec.ts b/apps/web/e2e/tests/automations-settings.spec.ts
index eefd30d9c0..e2b71d8554 100644
--- a/apps/web/e2e/tests/automations-settings.spec.ts
+++ b/apps/web/e2e/tests/automations-settings.spec.ts
@@ -227,6 +227,32 @@ test.describe("Automations settings page", () => {
});
});
+ test("create page keeps scrolling inside the settings pane", async ({ testPage, seedData }) => {
+ const automations = new AutomationsPage(testPage, seedData.workspaceId);
+ await automations.gotoNew();
+
+ const rootScrollStyles = await testPage.evaluate(() => ({
+ htmlOverflowY: getComputedStyle(document.documentElement).overflowY,
+ htmlOverscrollY: getComputedStyle(document.documentElement).overscrollBehaviorY,
+ bodyOverflowY: getComputedStyle(document.body).overflowY,
+ bodyOverscrollY: getComputedStyle(document.body).overscrollBehaviorY,
+ }));
+ expect(rootScrollStyles).toEqual({
+ htmlOverflowY: "hidden",
+ htmlOverscrollY: "none",
+ bodyOverflowY: "hidden",
+ bodyOverscrollY: "none",
+ });
+
+ const settingsScroller = testPage.getByTestId("settings-scroll-container");
+ await expect(settingsScroller).toBeVisible();
+ await expect(settingsScroller).toHaveCSS("overflow-y", "auto");
+ await expect(settingsScroller).toHaveCSS("overscroll-behavior-y", "contain");
+
+ await testPage.mouse.wheel(0, 3000);
+ await expect.poll(() => testPage.evaluate(() => window.scrollY), { timeout: 5_000 }).toBe(0);
+ });
+
test("enable/disable toggle on list page", async ({ testPage, seedData }) => {
const automations = new AutomationsPage(testPage, seedData.workspaceId);
diff --git a/apps/web/e2e/tests/mobile-automations-scroll.spec.ts b/apps/web/e2e/tests/mobile-automations-scroll.spec.ts
new file mode 100644
index 0000000000..e877d50847
--- /dev/null
+++ b/apps/web/e2e/tests/mobile-automations-scroll.spec.ts
@@ -0,0 +1,51 @@
+import type { Locator, Page } from "@playwright/test";
+import { test, expect } from "../fixtures/test-base";
+import { AutomationsPage } from "../pages/automations-page";
+
+/** Swipe up on a scroll container — the touch equivalent of wheel-down at scroll bottom. */
+async function swipeUpOnElement(page: Page, element: Locator): Promise
{
+ const box = await element.boundingBox();
+ if (!box) throw new Error("scroll container has no bounding box");
+
+ const cdp = await page.context().newCDPSession(page);
+ const centerX = box.x + box.width / 2;
+ const startY = box.y + box.height - 20;
+ const endY = box.y + 20;
+
+ await cdp.send("Input.dispatchTouchEvent", {
+ type: "touchStart",
+ touchPoints: [{ x: centerX, y: startY }],
+ });
+ for (let i = 1; i <= 8; i++) {
+ const y = startY + ((endY - startY) * i) / 8;
+ await cdp.send("Input.dispatchTouchEvent", {
+ type: "touchMove",
+ touchPoints: [{ x: centerX, y }],
+ });
+ }
+ await cdp.send("Input.dispatchTouchEvent", {
+ type: "touchEnd",
+ touchPoints: [],
+ });
+}
+
+test.describe("Automations settings on mobile", () => {
+ test("create page does not hand off bottom overscroll to the document", async ({
+ testPage,
+ seedData,
+ }) => {
+ const automations = new AutomationsPage(testPage, seedData.workspaceId);
+ await automations.gotoNew();
+
+ const settingsScroller = testPage.getByTestId("settings-scroll-container");
+ await expect(settingsScroller).toBeVisible();
+ await expect(settingsScroller).toHaveCSS("overscroll-behavior-y", "contain");
+
+ await settingsScroller.evaluate((el) => {
+ el.scrollTop = el.scrollHeight;
+ });
+ await swipeUpOnElement(testPage, settingsScroller);
+
+ await expect.poll(() => testPage.evaluate(() => window.scrollY), { timeout: 5_000 }).toBe(0);
+ });
+});