From 5741cd15e2f45b944dd8603fc43c79fd64a26fd3 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 28 Aug 2026 16:39:47 -0700 Subject: [PATCH] inline demo --- docs/guides/best-practices/scroll-traps.mdx | 181 ++++++++++++++++++ sidebars.ts | 1 + src/components/ScrollTrapDemo/index.tsx | 172 +++++++++++++++++ .../ScrollTrapDemo/styles.module.css | 163 ++++++++++++++++ .../guides/best-practices/scroll-traps.mdx | 181 ++++++++++++++++++ versioned_sidebars/version-0.14-sidebars.json | 1 + 6 files changed, 699 insertions(+) create mode 100644 docs/guides/best-practices/scroll-traps.mdx create mode 100644 src/components/ScrollTrapDemo/index.tsx create mode 100644 src/components/ScrollTrapDemo/styles.module.css create mode 100644 versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx diff --git a/docs/guides/best-practices/scroll-traps.mdx b/docs/guides/best-practices/scroll-traps.mdx new file mode 100644 index 00000000..78eb5207 --- /dev/null +++ b/docs/guides/best-practices/scroll-traps.mdx @@ -0,0 +1,181 @@ +import ScrollTrapDemo from "@site/src/components/ScrollTrapDemo"; + +# Avoid scroll traps in inline apps + +Inline apps appear inside the Reddit feed. Users must be able to scroll past +them with a mouse wheel, trackpad, or touch gesture. + +A scroll trap happens when the feed stops moving while the pointer is over the +app. This can happen even when the inline app has no visible scrollbar. Common +causes include internal scroll panels, full-surface canvases, games, maps, +carousels, and broad gesture handlers. + +Use inline mode for quick, bounded interactions. Move scrolling content, drag +gestures, zooming, maps, drawing canvases, and long flows into expanded mode. + + + +The demo shows two rejected patterns and one acceptable pattern. + +## What gets rejected + +An inline app will be rejected when it: + +- Uses `preventDefault()` on `wheel`, `touchmove`, or pointer gestures across + the full app surface. +- Blocks feed scrolling from a canvas, game board, map, carousel, or gesture + area even when the app itself has no internal scroll position. +- Sets `touch-action: none` or `overscroll-behavior: none` on `html`, `body`, + the root app container, or a full-surface canvas in inline mode. +- Sets `overflow: auto` or `overflow: scroll` on the inline app and requires the + user to scroll inside the post. +- Captures trackpad or mouse-wheel input for game controls while the app is + inline in the feed. +- Creates a tall inline experience where important content is only reachable by + scrolling inside the webview. + +The test is user-visible behavior, not CSS alone. `overflow: hidden` does not +fix the issue if JavaScript or CSS still blocks scroll gestures. + +CSS can cause the same issue without a JavaScript wheel handler: + +```css +/* Avoid this in inline mode. */ +html, +body { + height: 100%; + overflow: hidden; + overscroll-behavior: none; + touch-action: none; +} + +canvas { + touch-action: none; +} +``` + +This can block native page scrolling even if the app itself does not scroll. + +## What is acceptable + +Inline apps should: + +- Fit their important content inside the inline viewport. +- Let normal wheel and touch scrolling pass through to Reddit. +- Use taps, buttons, keyboard controls, or small bounded interactions instead of + page-like scrolling. +- Provide a clear path to open expanded mode when the experience needs more + space or richer gestures. + +Expanded mode can support deeper interaction because the user has intentionally +opened your app. That is the better home for large boards, galleries, maps, +editors, drawing surfaces, long settings panels, or content feeds. + +## How to fix it + +Remove broad scroll interception from the inline entry point: + +```ts +// Avoid this in inline mode. +window.addEventListener( + "wheel", + (event) => { + event.preventDefault(); + updateGameFromWheel(event.deltaY); + }, + { passive: false }, +); +``` + +Do not block wheel events on fixed surfaces: + +```ts +// Also avoid this in inline mode. +canvas.addEventListener( + "wheel", + (event) => { + event.preventDefault(); + zoomBoard(event.deltaY); + }, + { passive: false }, +); +``` + +Use explicit controls instead: + +```tsx +export function InlineControls() { + return ( +
+ + + +
+ ); +} +``` + +Allow vertical pan gestures in inline mode: + +```css +.inlineApp { + block-size: 100%; + overflow: hidden; + overscroll-behavior: auto; + touch-action: pan-y; +} + +.inlineAppCanvas { + aspect-ratio: 16 / 9; + max-block-size: 100%; + touch-action: pan-y; +} +``` + +`touch-action: pan-y` keeps vertical feed scrolling available while still +allowing taps and clicks inside the inline app. + +Put internal scrollers and full gesture controls in expanded mode: + +```json +{ + "post": { + "entrypoints": { + "inline": "dist/inline.html", + "expanded": "dist/expanded.html" + } + } +} +``` + +Reserve full gesture locking for expanded mode: + +```css +html.is-expanded, +html.is-expanded body, +html.is-expanded canvas { + overscroll-behavior: none; + touch-action: none; +} +``` + +Apply that class only in expanded mode. + +## Review checklist + +Before submitting, test your post in a real feed: + +- Hover over every part of the inline app and scroll with a mouse wheel or + trackpad. +- Swipe over the inline app on mobile and confirm the feed moves naturally. +- Inspect inline CSS for `touch-action: none` and `overscroll-behavior: none` + on `html`, `body`, the root app node, and full-surface canvases. +- Confirm the inline app still works when gestures are replaced by buttons, + taps, or keyboard controls. +- Move any experience that needs its own scroll position to expanded mode. diff --git a/sidebars.ts b/sidebars.ts index 6d18f15a..9181e529 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -306,6 +306,7 @@ const sidebars: SidebarsConfig = { label: "Best Practices", items: [ "guides/best-practices/community_games", + "guides/best-practices/scroll-traps", "guides/best-practices/mod_resources", "capabilities/server/text_fallback", ], diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx new file mode 100644 index 00000000..17d65bfb --- /dev/null +++ b/src/components/ScrollTrapDemo/index.tsx @@ -0,0 +1,172 @@ +import React, { useEffect, useRef, useState } from "react"; + +import styles from "./styles.module.css"; + +type Example = "internalScroll" | "gestureLock" | "fixed"; + +const examples: Array<{ + id: Example; + label: string; +}> = [ + { + id: "internalScroll", + label: "Internal scroll", + }, + { + id: "gestureLock", + label: "No scrollbar trap", + }, + { + id: "fixed", + label: "Fixed", + }, +]; + +export default function ScrollTrapDemo(): React.ReactElement { + const [activeExample, setActiveExample] = useState("internalScroll"); + const gestureTrapRef = useRef(null); + + useEffect(() => { + const addWheelTrap = (element: HTMLDivElement | null) => { + if (!element) { + return undefined; + } + + const onWheel = (event: WheelEvent) => { + event.preventDefault(); + }; + + element.addEventListener("wheel", onWheel, { passive: false }); + return () => element.removeEventListener("wheel", onWheel); + }; + + const removeGestureTrap = addWheelTrap(gestureTrapRef.current); + + return () => { + removeGestureTrap?.(); + }; + }, [activeExample]); + + return ( +
+
+ {examples.map((example) => ( + + ))} +
+ +
+

+ Hover the mock app and try to scroll the feed. +

+ + + {activeExample === "internalScroll" ? ( +
+
Rejected
+

Internal app scroll blocks the feed

+

+ The inline app has its own scrollable content. When the inner + panel reaches the top or bottom, the feed still does not take + over. +

+
+
Round 1: Choose a loadout
+
Round 2: Pick a path
+
Round 3: Claim a reward
+
Round 4: Open the shop
+
Round 5: Upgrade an item
+
Round 6: Start another run
+
Round 7: Check the leaderboard
+
Round 8: Invite a friend
+
+ + overscroll-behavior: contain; + +
+ ) : null} + + {activeExample === "gestureLock" ? ( +
+
Rejected
+

No scrollbar, still trapped

+

+ A full-surface game, canvas, or map uses gesture-locking CSS. It + looks fixed, but scroll input cannot escape the inline surface. +

+
Fixed app surface
+ + touch-action: none; overscroll-behavior: none; + +
+ ) : null} + + {activeExample === "fixed" ? ( +
+
Acceptable
+

Inline lets the feed scroll

+

+ The inline view is fixed-height and uses buttons or taps for + interaction. Vertical scroll gestures remain available to + Reddit. +

+
+ Fixed preview with no internal scroll +
+ + touch-action: pan-y; overscroll-behavior: auto; + +
+ ) : null} +
+
+
+ ); +} + +function MockFeed({ children }: { children: React.ReactNode }) { + return ( +
+
+ Feed item before the app +
+ + {children} + +
+ Feed item after the app +
+
+ ); +} diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css new file mode 100644 index 00000000..ba5967f4 --- /dev/null +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -0,0 +1,163 @@ +.wrapper { + border: 1px solid var(--ifm-toc-border-color); + border-radius: 8px; + margin: 1.5rem 0; + overflow: hidden; +} + +.tabs { + background: var(--ifm-background-surface-color); + border-bottom: 1px solid var(--ifm-toc-border-color); + display: flex; + gap: 0.5rem; + overflow-x: auto; + padding: 0.75rem; +} + +.tab, +.activeTab { + border: 1px solid var(--ifm-toc-border-color); + border-radius: 6px; + cursor: pointer; + flex: 0 0 auto; + font: inherit; + min-height: 2.25rem; + padding: 0.35rem 0.75rem; +} + +.tab { + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); +} + +.activeTab { + background: #ff4500; + color: #ffffff; +} + +.panel { + padding: 1rem; +} + +.instructions { + color: var(--ifm-color-emphasis-700); + font-size: 0.9rem; + margin: 0 auto 0.75rem; + max-width: 740px; +} + +.feed { + background: var(--ifm-background-color); + border: 1px solid var(--ifm-toc-border-color); + border-radius: 8px; + height: 657px; + margin: 0 auto; + max-height: 72vh; + max-width: 740px; + overflow-y: auto; + padding: 1rem; + width: 100%; +} + +.feedItem, +.app { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-toc-border-color); + border-radius: 8px; + margin: 0 auto 1rem; + padding: 1rem; +} + +.feedItem { + align-items: center; + color: var(--ifm-color-emphasis-600); + display: flex; + justify-content: center; + min-height: 30rem; + text-align: center; +} + +.app { + min-height: 30rem; +} + +.rejectedApp { + border-color: #d93a00; +} + +.acceptableApp { + border-color: #168a5b; + overscroll-behavior: auto; + touch-action: pan-y; +} + +.appLabel { + color: var(--ifm-color-emphasis-700); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + margin-bottom: 0.35rem; + text-transform: uppercase; +} + +.app h3 { + font-size: 1.35rem; + margin: 0 0 0.5rem; +} + +.app p { + margin-bottom: 0.75rem; +} + +.innerScroller { + border: 1px solid var(--ifm-toc-border-color); + border-radius: 6px; + margin-bottom: 0.75rem; + max-height: 16rem; + overscroll-behavior: contain; + overflow-y: auto; +} + +.innerScroller div { + border-bottom: 1px solid var(--ifm-toc-border-color); + padding: 0.65rem 0.75rem; +} + +.innerScroller div:last-child { + border-bottom: 0; +} + +.fixedSurface { + align-items: center; + border: 1px solid var(--ifm-toc-border-color); + border-radius: 6px; + color: var(--ifm-color-emphasis-600); + display: flex; + justify-content: center; + margin-bottom: 0.75rem; + min-height: 16rem; + touch-action: none; +} + +.codeLine { + display: block; + font-size: 0.8rem; + line-height: 1.4; + overflow-wrap: anywhere; + padding: 0.35rem 0.45rem; +} + +@media (max-width: 640px) { + .feed { + height: 32rem; + max-height: 70vh; + } + + .app { + min-height: 26rem; + } + + .feedItem { + min-height: 26rem; + } +} diff --git a/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx b/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx new file mode 100644 index 00000000..78eb5207 --- /dev/null +++ b/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx @@ -0,0 +1,181 @@ +import ScrollTrapDemo from "@site/src/components/ScrollTrapDemo"; + +# Avoid scroll traps in inline apps + +Inline apps appear inside the Reddit feed. Users must be able to scroll past +them with a mouse wheel, trackpad, or touch gesture. + +A scroll trap happens when the feed stops moving while the pointer is over the +app. This can happen even when the inline app has no visible scrollbar. Common +causes include internal scroll panels, full-surface canvases, games, maps, +carousels, and broad gesture handlers. + +Use inline mode for quick, bounded interactions. Move scrolling content, drag +gestures, zooming, maps, drawing canvases, and long flows into expanded mode. + + + +The demo shows two rejected patterns and one acceptable pattern. + +## What gets rejected + +An inline app will be rejected when it: + +- Uses `preventDefault()` on `wheel`, `touchmove`, or pointer gestures across + the full app surface. +- Blocks feed scrolling from a canvas, game board, map, carousel, or gesture + area even when the app itself has no internal scroll position. +- Sets `touch-action: none` or `overscroll-behavior: none` on `html`, `body`, + the root app container, or a full-surface canvas in inline mode. +- Sets `overflow: auto` or `overflow: scroll` on the inline app and requires the + user to scroll inside the post. +- Captures trackpad or mouse-wheel input for game controls while the app is + inline in the feed. +- Creates a tall inline experience where important content is only reachable by + scrolling inside the webview. + +The test is user-visible behavior, not CSS alone. `overflow: hidden` does not +fix the issue if JavaScript or CSS still blocks scroll gestures. + +CSS can cause the same issue without a JavaScript wheel handler: + +```css +/* Avoid this in inline mode. */ +html, +body { + height: 100%; + overflow: hidden; + overscroll-behavior: none; + touch-action: none; +} + +canvas { + touch-action: none; +} +``` + +This can block native page scrolling even if the app itself does not scroll. + +## What is acceptable + +Inline apps should: + +- Fit their important content inside the inline viewport. +- Let normal wheel and touch scrolling pass through to Reddit. +- Use taps, buttons, keyboard controls, or small bounded interactions instead of + page-like scrolling. +- Provide a clear path to open expanded mode when the experience needs more + space or richer gestures. + +Expanded mode can support deeper interaction because the user has intentionally +opened your app. That is the better home for large boards, galleries, maps, +editors, drawing surfaces, long settings panels, or content feeds. + +## How to fix it + +Remove broad scroll interception from the inline entry point: + +```ts +// Avoid this in inline mode. +window.addEventListener( + "wheel", + (event) => { + event.preventDefault(); + updateGameFromWheel(event.deltaY); + }, + { passive: false }, +); +``` + +Do not block wheel events on fixed surfaces: + +```ts +// Also avoid this in inline mode. +canvas.addEventListener( + "wheel", + (event) => { + event.preventDefault(); + zoomBoard(event.deltaY); + }, + { passive: false }, +); +``` + +Use explicit controls instead: + +```tsx +export function InlineControls() { + return ( +
+ + + +
+ ); +} +``` + +Allow vertical pan gestures in inline mode: + +```css +.inlineApp { + block-size: 100%; + overflow: hidden; + overscroll-behavior: auto; + touch-action: pan-y; +} + +.inlineAppCanvas { + aspect-ratio: 16 / 9; + max-block-size: 100%; + touch-action: pan-y; +} +``` + +`touch-action: pan-y` keeps vertical feed scrolling available while still +allowing taps and clicks inside the inline app. + +Put internal scrollers and full gesture controls in expanded mode: + +```json +{ + "post": { + "entrypoints": { + "inline": "dist/inline.html", + "expanded": "dist/expanded.html" + } + } +} +``` + +Reserve full gesture locking for expanded mode: + +```css +html.is-expanded, +html.is-expanded body, +html.is-expanded canvas { + overscroll-behavior: none; + touch-action: none; +} +``` + +Apply that class only in expanded mode. + +## Review checklist + +Before submitting, test your post in a real feed: + +- Hover over every part of the inline app and scroll with a mouse wheel or + trackpad. +- Swipe over the inline app on mobile and confirm the feed moves naturally. +- Inspect inline CSS for `touch-action: none` and `overscroll-behavior: none` + on `html`, `body`, the root app node, and full-surface canvases. +- Confirm the inline app still works when gestures are replaced by buttons, + taps, or keyboard controls. +- Move any experience that needs its own scroll position to expanded mode. diff --git a/versioned_sidebars/version-0.14-sidebars.json b/versioned_sidebars/version-0.14-sidebars.json index 2991d31f..030c70fc 100644 --- a/versioned_sidebars/version-0.14-sidebars.json +++ b/versioned_sidebars/version-0.14-sidebars.json @@ -293,6 +293,7 @@ "label": "Best Practices", "items": [ "guides/best-practices/community_games", + "guides/best-practices/scroll-traps", "guides/best-practices/mod_resources", "capabilities/server/text_fallback" ]