From 5741cd15e2f45b944dd8603fc43c79fd64a26fd3 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 28 Aug 2026 16:39:47 -0700 Subject: [PATCH 1/5] 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" ] From c7fe8e1b4c2d31575376ee8fea1e3188749851cd Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 4 Sep 2026 10:11:02 -0700 Subject: [PATCH 2/5] jeevs suggestions --- docs/guides/best-practices/scroll-traps.mdx | 61 +++++++++---------- .../guides/best-practices/scroll-traps.mdx | 61 +++++++++---------- 2 files changed, 58 insertions(+), 64 deletions(-) diff --git a/docs/guides/best-practices/scroll-traps.mdx b/docs/guides/best-practices/scroll-traps.mdx index 78eb5207..c0c62908 100644 --- a/docs/guides/best-practices/scroll-traps.mdx +++ b/docs/guides/best-practices/scroll-traps.mdx @@ -10,8 +10,9 @@ 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. +Use inline mode for quick, bounded interactions. Move vertical scrolling, +full-surface drag gestures, zooming, maps, drawing canvases, and long flows into +expanded mode. @@ -31,16 +32,17 @@ An inline app will be rejected when it: 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 +- Places important content outside the visible inline area and requires vertical 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. +Review the user-visible behavior. If JavaScript or CSS blocks scroll gestures, +the inline app will fail even with `overflow: hidden`. CSS can cause the same issue without a JavaScript wheel handler: +#### Don't + ```css -/* Avoid this in inline mode. */ html, body { height: 100%; @@ -62,21 +64,25 @@ 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. +- Use taps, buttons, or small bounded interactions for inline controls. - 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. +Scroll trap restrictions apply to inline apps in the feed. Expanded mode is the +better home for large boards, galleries, maps, editors, drawing surfaces, +settings panels, and content feeds. + +Test on web, Android, and iOS. Vertical feed scrolling must work on every +platform. Horizontal gestures can behave differently by platform, so keep them +bounded and verify that they do not block vertical feed scrolling. ## How to fix it Remove broad scroll interception from the inline entry point: +#### Don't + ```ts -// Avoid this in inline mode. window.addEventListener( "wheel", (event) => { @@ -89,8 +95,9 @@ window.addEventListener( Do not block wheel events on fixed surfaces: +#### Don't + ```ts -// Also avoid this in inline mode. canvas.addEventListener( "wheel", (event) => { @@ -103,6 +110,8 @@ canvas.addEventListener( Use explicit controls instead: +#### Do + ```tsx export function InlineControls() { return ( @@ -113,15 +122,14 @@ export function InlineControls() { - ); } ``` -Allow vertical pan gestures in inline mode: +If you set gesture CSS in inline mode, allow vertical pan gestures: + +#### Do ```css .inlineApp { @@ -154,18 +162,7 @@ Put internal scrollers and full gesture controls in expanded mode: } ``` -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. +Reserve full gesture locking for expanded routes. ## Review checklist @@ -176,6 +173,6 @@ Before submitting, test your post in a real feed: - 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. +- Confirm the inline app does not prevent default behavior for arrow keys, + space, Page Up, or Page Down. +- Move any experience that needs vertical scrolling to expanded mode. 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 index 78eb5207..c0c62908 100644 --- a/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx +++ b/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx @@ -10,8 +10,9 @@ 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. +Use inline mode for quick, bounded interactions. Move vertical scrolling, +full-surface drag gestures, zooming, maps, drawing canvases, and long flows into +expanded mode. @@ -31,16 +32,17 @@ An inline app will be rejected when it: 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 +- Places important content outside the visible inline area and requires vertical 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. +Review the user-visible behavior. If JavaScript or CSS blocks scroll gestures, +the inline app will fail even with `overflow: hidden`. CSS can cause the same issue without a JavaScript wheel handler: +#### Don't + ```css -/* Avoid this in inline mode. */ html, body { height: 100%; @@ -62,21 +64,25 @@ 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. +- Use taps, buttons, or small bounded interactions for inline controls. - 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. +Scroll trap restrictions apply to inline apps in the feed. Expanded mode is the +better home for large boards, galleries, maps, editors, drawing surfaces, +settings panels, and content feeds. + +Test on web, Android, and iOS. Vertical feed scrolling must work on every +platform. Horizontal gestures can behave differently by platform, so keep them +bounded and verify that they do not block vertical feed scrolling. ## How to fix it Remove broad scroll interception from the inline entry point: +#### Don't + ```ts -// Avoid this in inline mode. window.addEventListener( "wheel", (event) => { @@ -89,8 +95,9 @@ window.addEventListener( Do not block wheel events on fixed surfaces: +#### Don't + ```ts -// Also avoid this in inline mode. canvas.addEventListener( "wheel", (event) => { @@ -103,6 +110,8 @@ canvas.addEventListener( Use explicit controls instead: +#### Do + ```tsx export function InlineControls() { return ( @@ -113,15 +122,14 @@ export function InlineControls() { - ); } ``` -Allow vertical pan gestures in inline mode: +If you set gesture CSS in inline mode, allow vertical pan gestures: + +#### Do ```css .inlineApp { @@ -154,18 +162,7 @@ Put internal scrollers and full gesture controls in expanded mode: } ``` -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. +Reserve full gesture locking for expanded routes. ## Review checklist @@ -176,6 +173,6 @@ Before submitting, test your post in a real feed: - 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. +- Confirm the inline app does not prevent default behavior for arrow keys, + space, Page Up, or Page Down. +- Move any experience that needs vertical scrolling to expanded mode. From a61b65b67e0575d58f6ba3b04c4ff43695822fc7 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 4 Sep 2026 16:20:54 -0700 Subject: [PATCH 3/5] redesigned --- docs/guides/best-practices/scroll-traps.mdx | 2 + src/components/ScrollTrapDemo/index.tsx | 318 +++++++++--- .../ScrollTrapDemo/styles.module.css | 459 +++++++++++++++--- src/css/custom.css | 9 + .../guides/best-practices/scroll-traps.mdx | 2 + 5 files changed, 637 insertions(+), 153 deletions(-) diff --git a/docs/guides/best-practices/scroll-traps.mdx b/docs/guides/best-practices/scroll-traps.mdx index c0c62908..cc6b8207 100644 --- a/docs/guides/best-practices/scroll-traps.mdx +++ b/docs/guides/best-practices/scroll-traps.mdx @@ -14,6 +14,8 @@ Use inline mode for quick, bounded interactions. Move vertical scrolling, full-surface drag gestures, zooming, maps, drawing canvases, and long flows into expanded mode. +## Inline scroll trap demo + The demo shows two rejected patterns and one acceptable pattern. diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index 17d65bfb..e95fc142 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -24,8 +24,15 @@ const examples: Array<{ export default function ScrollTrapDemo(): React.ReactElement { const [activeExample, setActiveExample] = useState("internalScroll"); + const internalScrollRef = useRef(null); const gestureTrapRef = useRef(null); + useEffect(() => { + if (activeExample === "internalScroll") { + internalScrollRef.current?.focus({ preventScroll: true }); + } + }, [activeExample]); + useEffect(() => { const addWheelTrap = (element: HTMLDivElement | null) => { if (!element) { @@ -75,98 +82,251 @@ export default function ScrollTrapDemo(): React.ReactElement { aria-labelledby={`scroll-trap-tab-${activeExample}`} >

- Hover the mock app and try to scroll the feed. + Hover over the mock app and scroll. Rejected examples stop the mock + feed. The fixed example lets the feed move.

- - {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} -
+
+
+ + + {activeExample === "internalScroll" ? ( + + ) : null} + + {activeExample === "gestureLock" ? ( + + ) : null} + + {activeExample === "fixed" ? : null} + + +
+
); } -function MockFeed({ children }: { children: React.ReactNode }) { +function PlainMockPost({ + comments, + label, + title, + votes, +}: { + comments: string; + label: string; + title: string; + votes: string; +}) { return ( -
-
- Feed item before the app +
+ +

{title}

+ Community App +
+
+ {label} + Preview +
+
+ + + + +
+ +
+ ); +} +function MockPost({ children }: { children: React.ReactNode }) { + return ( +
+ +

Daily Puzzle #116

+ Daily Game {children} + +
+ ); +} -
- Feed item after the app +function PostMeta({ + avatar, + name, + time, +}: { + avatar: string; + name: string; + time: string; +}) { + return ( +
+ {avatar} + {name} + App + {time} + ... +
+ ); +} + +function PostFooter({ comments, votes }: { comments: string; votes: string }) { + return ( +
+ + + Privacy + +
+ + + {votes} + + + + + {comments} + + + + + + + Share +
); } + +function InternalScrollApp({ + scrollerRef, +}: { + scrollerRef: React.RefObject; +}) { + return ( +
+ +
+
+

Internal app scroll blocks the feed

+

+ The app contains its own scrolling panel. When that panel reaches an + edge, the Reddit feed still cannot continue scrolling. +

+
scrollerRef.current?.focus()} + ref={scrollerRef} + tabIndex={0} + > +
+ Daily run complete + Score 186 points and keep a 4 day streak. +
+ + + + + + + +
+ overscroll-behavior: contain; +
+
+
+ ); +} + +const GestureLockApp = React.forwardRef( + function GestureLockApp(_props, ref) { + return ( +
+ +
+
+

No scrollbar, still trapped

+

+ The surface is fixed, but it locks gestures across the whole + inline app. The feed cannot use the wheel or touch input. +

+
+
+ Fixed canvas or game area +
+
+ touch-action: none; overscroll-behavior: none; +
+
+
+ ); + }, +); + +function FixedApp() { + return ( +
+ +
+
+

Inline lets the feed scroll

+

+ The inline view fits in the post. It uses taps or buttons for + interaction and allows vertical feed scrolling. +

+
+
+ Fixed preview with no scroll +
+
+ touch-action: pan-y; overscroll-behavior: auto; +
+
+
+ ); +} + +function AppToolbar({ status }: { status: "Rejected" | "Acceptable" }) { + return ( +
+
+ r/ + ? +
+ + {status} + +
+ ); +} diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index ba5967f4..88ff60b9 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -1,163 +1,474 @@ .wrapper { - border: 1px solid var(--ifm-toc-border-color); + background: #071012; + border: 1px solid #263438; 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); + background: #050b0d; + border-bottom: 1px solid #263438; display: flex; - gap: 0.5rem; + gap: 0.75rem; overflow-x: auto; - padding: 0.75rem; + padding: 1rem; } .tab, .activeTab { - border: 1px solid var(--ifm-toc-border-color); - border-radius: 6px; + background: #071012; + border: 1px solid #405056; + border-radius: 8px; + color: #f2f5f6; cursor: pointer; flex: 0 0 auto; font: inherit; - min-height: 2.25rem; - padding: 0.35rem 0.75rem; + font-weight: 700; + min-height: 2.6rem; + padding: 0.45rem 1rem; } -.tab { - background: var(--ifm-background-color); - color: var(--ifm-font-color-base); +.tab:hover { + border-color: #ff4500; + color: #ff4500; } .activeTab { - background: #ff4500; - color: #ffffff; + border-color: #ff4500; + box-shadow: inset 0 0 0 1px #ff4500; + color: #ff4500; +} + +.tab:focus-visible, +.activeTab:focus-visible { + outline: 2px solid #ff4500; + outline-offset: 2px; } .panel { + background: #071012; padding: 1rem; } .instructions { - color: var(--ifm-color-emphasis-700); + color: #c7d0d4; 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); +.feedViewport { + background: #071012; + border: 1px solid #263438; border-radius: 8px; - height: 657px; + height: 760px; margin: 0 auto; - max-height: 72vh; max-width: 740px; + overflow-x: hidden; overflow-y: auto; - padding: 1rem; + padding: 0.75rem; 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; +.feedCanvas { + transform: scale(0.76); + transform-origin: top left; + width: 131.6%; +} + +.post { + background: #0b1416; + border-top: 1px solid #253136; + color: #f2f5f6; + margin: 0 0 1rem; + padding: 0.75rem 0.65rem 0.9rem; } -.feedItem { +.postMeta { align-items: center; - color: var(--ifm-color-emphasis-600); + color: #9aa7ad; display: flex; + gap: 0.35rem; + margin-bottom: 0.75rem; +} + +.avatar { + align-items: center; + background: #ff4500; + border: 2px solid #ffffff; + border-radius: 999px; + color: #ffffff; + display: inline-flex; + font-size: 0.8rem; + font-weight: 700; + height: 1.75rem; justify-content: center; - min-height: 30rem; - text-align: center; + width: 1.75rem; +} + +.postMeta strong { + color: #d5dcdf; } -.app { - min-height: 30rem; +.appBadge { + border: 1px solid #526167; + border-radius: 999px; + color: #c7d0d4; + padding: 0 0.45rem; } -.rejectedApp { - border-color: #d93a00; +.moreButton { + margin-left: auto; } -.acceptableApp { - border-color: #168a5b; +.postTitle { + color: #f2f5f6; + font-size: 1.35rem; + margin: 0 0 0.65rem; +} + +.flair { + background: #ffd635; + border-radius: 999px; + color: #111111; + display: inline-block; + font-size: 0.85rem; + margin-bottom: 1rem; + padding: 0.15rem 0.6rem; +} + +.appSurface { + background: #ffffff; + border-radius: 16px; + color: #111111; + display: flex; + flex-direction: column; + min-height: 657px; + overflow: hidden; + padding: 1rem; +} + +.plainPost { + opacity: 0.92; +} + +.plainPost .postTitle { + font-size: 1.15rem; +} + +.plainPost .flair { + margin-bottom: 0.65rem; +} + +.plainPost .postFooter { + padding-top: 0.65rem; +} + +.plainAppSurface { + background: #f6f7f8; + border: 1px solid #d7dde1; + border-radius: 16px; + color: #1a1a1b; + display: flex; + flex-direction: column; + min-height: 657px; + overflow: hidden; + padding: 0.8rem; +} + +.plainAppHeader { + align-items: center; + border-bottom: 1px solid #d7dde1; + display: flex; + justify-content: space-between; + margin: -0.8rem -0.8rem 0.8rem; + padding: 0.8rem; +} + +.plainAppHeader strong { + font-size: 1rem; +} + +.plainAppHeader span { + color: #667178; + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; +} + +.plainAppGrid { + display: grid; + flex: 1; + gap: 0.6rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.plainAppGrid span { + background: #ffffff; + border: 1px solid #d7dde1; + border-radius: 8px; + min-height: 10rem; +} + +.rejectedSurface { + outline: 3px solid #ff4500; +} + +.acceptableSurface { + outline: 3px solid #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; +.appToolbar { + align-items: center; + display: flex; + justify-content: space-between; +} + +.appBody { + align-items: center; + display: flex; + flex: 1; + padding: 2rem 1rem 1rem; +} + +.iconButton { + align-items: center; + background: #ffffff; + border: 3px solid #111111; + border-radius: 999px; + display: inline-flex; + font-weight: 800; + height: 2.3rem; + justify-content: center; + margin-right: 0.45rem; + width: 2.3rem; +} + +.rejectedPill, +.acceptablePill { + border: 2px solid #111111; + border-radius: 999px; + color: #111111; + font-size: 0.8rem; + font-weight: 800; + padding: 0.2rem 0.65rem; text-transform: uppercase; } -.app h3 { +.rejectedPill { + background: #ffd1c2; +} + +.acceptablePill { + background: #caf4d0; +} + +.centerPanel { + background: #ffffff; + border: 3px dashed #8f8f8f; + border-radius: 12px; + margin: 0; + padding: 1.2rem; + text-align: center; + width: 100%; +} + +.centerPanel h4 { font-size: 1.35rem; margin: 0 0 0.5rem; } -.app p { - margin-bottom: 0.75rem; +.centerPanel p { + color: #343a3d; + margin: 0 0 1rem; +} + +.centerPanel code { + display: block; + font-size: 0.78rem; + overflow-wrap: anywhere; + padding: 0.45rem 0.55rem; + text-align: left; } .innerScroller { - border: 1px solid var(--ifm-toc-border-color); - border-radius: 6px; - margin-bottom: 0.75rem; - max-height: 16rem; + border: 2px solid #111111; + border-radius: 8px; + margin-bottom: 0.9rem; + max-height: 19rem; overscroll-behavior: contain; overflow-y: auto; + text-align: left; + width: 100%; } .innerScroller div { - border-bottom: 1px solid var(--ifm-toc-border-color); - padding: 0.65rem 0.75rem; + border-bottom: 1px solid #c8ced2; + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.85rem 1rem; } -.innerScroller div:last-child { - border-bottom: 0; +.innerScroller button { + align-items: center; + background: #ff4500; + border: 2px solid #111111; + border-radius: 10px; + box-shadow: 4px 4px 0 #111111; + color: #111111; + cursor: pointer; + display: flex; + font: inherit; + font-weight: 800; + justify-content: center; + margin: 0.85rem 1rem; + min-height: 5rem; + padding: 1rem; + text-align: center; + width: calc(100% - 2rem); } -.fixedSurface { +.innerScroller button:hover { + background: #ff6a33; +} + +.innerScroller strong { + font-size: 1rem; +} + +.innerScroller span { + color: #4f5960; + font-size: 0.88rem; +} + +.previewBoard { 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; + min-height: 19rem; touch-action: none; } -.codeLine { - display: block; - font-size: 0.8rem; - line-height: 1.4; - overflow-wrap: anywhere; - padding: 0.35rem 0.45rem; +.previewCard { + align-items: center; + background: #ff4500; + border: 2px solid #111111; + border-radius: 12px; + color: #111111; + display: flex; + font-weight: 800; + justify-content: center; + min-height: 6rem; + padding: 1rem; + width: min(100%, 24rem); +} + +.postFooter { + align-items: flex-end; + color: #d5dcdf; + display: flex; + flex-direction: column; + gap: 0.75rem; + padding-top: 0.9rem; +} + +.privacyLink { + align-items: center; + display: inline-flex; + font-weight: 700; + gap: 0.35rem; +} + +.footerActions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.7rem; + width: 100%; +} + +.votePill, +.actionPill { + align-items: center; + background: #16231b; + border-radius: 999px; + color: #f2f5f6; + display: inline-flex; + font-weight: 700; + gap: 0.45rem; + min-height: 2.35rem; + padding: 0.35rem 0.75rem; +} + +.icon { + background: currentColor; + display: inline-block; + height: 1.25rem; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + -webkit-mask-position: center; + -webkit-mask-repeat: no-repeat; + -webkit-mask-size: contain; + width: 1.25rem; +} + +.upvoteIcon { + mask-image: url("../../../docs/assets/icons/icon-upvote-outline.svg"); + -webkit-mask-image: url("../../../docs/assets/icons/icon-upvote-outline.svg"); +} + +.downvoteIcon { + mask-image: url("../../../docs/assets/icons/icon-downvote-outline.svg"); + -webkit-mask-image: url("../../../docs/assets/icons/icon-downvote-outline.svg"); +} + +.commentIcon { + mask-image: url("../../../docs/assets/icons/icon-comment-outline.svg"); + -webkit-mask-image: url("../../../docs/assets/icons/icon-comment-outline.svg"); +} + +.refreshIcon { + mask-image: url("../../../docs/assets/icons/icon-refresh-outline.svg"); + -webkit-mask-image: url("../../../docs/assets/icons/icon-refresh-outline.svg"); +} + +.shareIcon { + mask-image: url("../../../docs/assets/icons/icon-share-new-outline.svg"); + -webkit-mask-image: url("../../../docs/assets/icons/icon-share-new-outline.svg"); +} + +.infoIcon { + height: 1rem; + mask-image: url("../../../docs/assets/icons/icon-info-outline.svg"); + -webkit-mask-image: url("../../../docs/assets/icons/icon-info-outline.svg"); + width: 1rem; } @media (max-width: 640px) { - .feed { - height: 32rem; - max-height: 70vh; + .feedViewport { + height: 38rem; + max-height: 76vh; + } + + .feedCanvas { + transform: scale(0.8); + width: 125%; + } + + .appSurface { + min-height: 36rem; } - .app { - min-height: 26rem; + .plainAppSurface { + min-height: 36rem; } - .feedItem { - min-height: 26rem; + .centerPanel { + margin-top: 0; } } diff --git a/src/css/custom.css b/src/css/custom.css index ccdba0d8..961d8da6 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -134,6 +134,15 @@ box-shadow: var(--ifm-global-shadow-lw); } +.theme-doc-markdown :is(h2, h3, h4, h5, h6) .hash-link { + opacity: 0.6; +} + +.theme-doc-markdown :is(h2, h3, h4, h5, h6) .hash-link:focus, +.theme-doc-markdown :is(h2, h3, h4, h5, h6):hover .hash-link { + opacity: 1; +} + /* Table styling */ table { 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 index c0c62908..cc6b8207 100644 --- a/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx +++ b/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx @@ -14,6 +14,8 @@ Use inline mode for quick, bounded interactions. Move vertical scrolling, full-surface drag gestures, zooming, maps, drawing canvases, and long flows into expanded mode. +## Inline scroll trap demo + The demo shows two rejected patterns and one acceptable pattern. From d2a18b83b12f8978371a3b2a2330fc5166b59f1d Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 4 Sep 2026 19:21:07 -0700 Subject: [PATCH 4/5] instructions added --- docs/guides/best-practices/scroll-traps.mdx | 4 +++ src/components/ScrollTrapDemo/index.tsx | 29 ++++++++++++------- .../ScrollTrapDemo/styles.module.css | 2 +- .../guides/best-practices/scroll-traps.mdx | 4 +++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/guides/best-practices/scroll-traps.mdx b/docs/guides/best-practices/scroll-traps.mdx index cc6b8207..9db1e263 100644 --- a/docs/guides/best-practices/scroll-traps.mdx +++ b/docs/guides/best-practices/scroll-traps.mdx @@ -16,6 +16,10 @@ expanded mode. ## Inline scroll trap demo +Below is an example feed with three tabs: an internal scroll trap, a +no-scrollbar trap, and a fixed version. Switch tabs, then scroll through the +feed to compare how each inline app handles scroll input. + The demo shows two rejected patterns and one acceptable pattern. diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index e95fc142..79071e81 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -5,20 +5,27 @@ import styles from "./styles.module.css"; type Example = "internalScroll" | "gestureLock" | "fixed"; const examples: Array<{ + description: string; id: Example; label: string; }> = [ { + description: + "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll, and the Reddit feed stops moving.", id: "internalScroll", - label: "Internal scroll", + label: "Internal scroll trap", }, { + description: + "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll even though no scrollbar is visible, and the Reddit feed stops moving.", id: "gestureLock", label: "No scrollbar trap", }, { + description: + "Start scrolling until you hit the app, then hover over it and scroll. The app does not capture the scroll, and the Reddit feed continues moving normally.", id: "fixed", - label: "Fixed", + label: "Feed stays scrollable", }, ]; @@ -26,6 +33,9 @@ export default function ScrollTrapDemo(): React.ReactElement { const [activeExample, setActiveExample] = useState("internalScroll"); const internalScrollRef = useRef(null); const gestureTrapRef = useRef(null); + const selectedExample = examples.find( + (example) => example.id === activeExample, + ); useEffect(() => { if (activeExample === "internalScroll") { @@ -81,15 +91,12 @@ export default function ScrollTrapDemo(): React.ReactElement { role="tabpanel" aria-labelledby={`scroll-trap-tab-${activeExample}`} > -

- Hover over the mock app and scroll. Rejected examples stop the mock - feed. The fixed example lets the feed move. -

+

{selectedExample?.description}

: null} - +

{title}

Community App
@@ -154,8 +161,8 @@ function PlainMockPost({ function MockPost({ children }: { children: React.ReactNode }) { return (
- -

Daily Puzzle #116

+ +

Daily Game #116

Daily Game {children} diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index 88ff60b9..42c5bff0 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -7,7 +7,7 @@ } .tabs { - background: #050b0d; + background: transparent; border-bottom: 1px solid #263438; display: flex; gap: 0.75rem; 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 index cc6b8207..9db1e263 100644 --- a/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx +++ b/versioned_docs/version-0.14/guides/best-practices/scroll-traps.mdx @@ -16,6 +16,10 @@ expanded mode. ## Inline scroll trap demo +Below is an example feed with three tabs: an internal scroll trap, a +no-scrollbar trap, and a fixed version. Switch tabs, then scroll through the +feed to compare how each inline app handles scroll input. + The demo shows two rejected patterns and one acceptable pattern. From afc0624c9c6bc561b8519fbc15914c88a6441ddc Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Mon, 7 Sep 2026 09:35:45 -0700 Subject: [PATCH 5/5] dark/light mode --- .../ScrollTrapDemo/styles.module.css | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index 42c5bff0..13c8f8c9 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -1,14 +1,36 @@ .wrapper { - background: #071012; - border: 1px solid #263438; + --demo-action-bg: #eef1f3; + --demo-border: #d7dde1; + --demo-muted-text: #57646b; + --demo-post-bg: #ffffff; + --demo-shell-bg: #f6f7f8; + --demo-surface-bg: #f2f4f5; + --demo-tab-bg: #ffffff; + --demo-tab-border: #c8ced2; + --demo-text: #1a1a1b; + + background: var(--demo-shell-bg); + border: 1px solid var(--demo-border); border-radius: 8px; margin: 1.5rem 0; overflow: hidden; } +:global([data-theme="dark"]) .wrapper { + --demo-action-bg: #16231b; + --demo-border: #263438; + --demo-muted-text: #c7d0d4; + --demo-post-bg: #0b1416; + --demo-shell-bg: #071012; + --demo-surface-bg: #071012; + --demo-tab-bg: #071012; + --demo-tab-border: #405056; + --demo-text: #f2f5f6; +} + .tabs { background: transparent; - border-bottom: 1px solid #263438; + border-bottom: 1px solid var(--demo-border); display: flex; gap: 0.75rem; overflow-x: auto; @@ -17,10 +39,10 @@ .tab, .activeTab { - background: #071012; - border: 1px solid #405056; + background: var(--demo-tab-bg); + border: 1px solid var(--demo-tab-border); border-radius: 8px; - color: #f2f5f6; + color: var(--demo-text); cursor: pointer; flex: 0 0 auto; font: inherit; @@ -47,20 +69,20 @@ } .panel { - background: #071012; + background: var(--demo-shell-bg); padding: 1rem; } .instructions { - color: #c7d0d4; + color: var(--demo-muted-text); font-size: 0.9rem; margin: 0 auto 0.75rem; max-width: 740px; } .feedViewport { - background: #071012; - border: 1px solid #263438; + background: var(--demo-surface-bg); + border: 1px solid var(--demo-border); border-radius: 8px; height: 760px; margin: 0 auto; @@ -78,16 +100,16 @@ } .post { - background: #0b1416; - border-top: 1px solid #253136; - color: #f2f5f6; + background: var(--demo-post-bg); + border-top: 1px solid var(--demo-border); + color: var(--demo-text); margin: 0 0 1rem; padding: 0.75rem 0.65rem 0.9rem; } .postMeta { align-items: center; - color: #9aa7ad; + color: var(--demo-muted-text); display: flex; gap: 0.35rem; margin-bottom: 0.75rem; @@ -108,13 +130,13 @@ } .postMeta strong { - color: #d5dcdf; + color: var(--demo-text); } .appBadge { - border: 1px solid #526167; + border: 1px solid var(--demo-tab-border); border-radius: 999px; - color: #c7d0d4; + color: var(--demo-muted-text); padding: 0 0.45rem; } @@ -123,7 +145,7 @@ } .postTitle { - color: #f2f5f6; + color: var(--demo-text); font-size: 1.35rem; margin: 0 0 0.65rem; } @@ -369,7 +391,7 @@ .postFooter { align-items: flex-end; - color: #d5dcdf; + color: var(--demo-text); display: flex; flex-direction: column; gap: 0.75rem; @@ -394,9 +416,9 @@ .votePill, .actionPill { align-items: center; - background: #16231b; + background: var(--demo-action-bg); border-radius: 999px; - color: #f2f5f6; + color: var(--demo-text); display: inline-flex; font-weight: 700; gap: 0.45rem;