Skip to content

fix(GlassPanes): upgrade off flat SSR fallback after mount#26

Open
yousefh409 wants to merge 1 commit into
mainfrom
fix/glasspanes-client-mount-detection
Open

fix(GlassPanes): upgrade off flat SSR fallback after mount#26
yousefh409 wants to merge 1 commit into
mainfrom
fix/glasspanes-client-mount-detection

Conversation

@yousefh409

@yousefh409 yousefh409 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

GlassPanes evaluated reduced-motion and backdrop-filter support during render. Both are static-safe during SSR and the first client render (assume reduced motion + no backdrop-filter), and neither reliably re-evaluated after mount — so in the browser the panes stayed stuck on the flat, non-animated fallback: no frosting, no drift (they looked like a foggy static wash).

Fix: gate both behind a mounted flag set in an effect, so the client re-renders once with the real values and upgrades to frosted, animated glass. Also widened the slide drift (±4% → ±13%) so motion is actually perceptible, and reduced the blur range (8–26px → 4–12px) so panes read as crisp glass rather than fog.

Verified in a real browser (Next 16 / React 19): panes now render frosted (data-fallback=false, backdrop-filter present) and drift visibly.

https://claude.ai/code/session_01WNP6tiqg78cXb7i23ThmQU


Open in Devin Review

Summary by cubic

Fixes GlassPanes staying on the flat SSR fallback by re-checking reduced-motion and backdrop-filter after mount. Also adjusts drift and blur so panes read as crisp, moving glass.

  • Bug Fixes
    • Re-evaluate reduced-motion and backdrop-filter support after mount via a mounted flag, upgrading from the SSR-safe flat fallback to frosted, animated panes when supported.
    • Motion polish: widen drift to ±13%, reduce blur to 4–12px, and slightly increase vertical slide to improve visibility and clarity.

Written for commit d1014fa. Summary will update on new commits.

Review in cubic

GlassPanes read reduced-motion and backdrop-filter support during render.
Both are static-safe during SSR/first client render (assume reduced motion
and no backdrop-filter), so on the client the panes stayed stranded on the
flat, non-animated fallback: no frosting and no drift.

Gate both behind a mounted flag set in an effect, so the client re-renders
once with the real values and upgrades to frosted, animated glass. Also
widen the drift (4% -> 13%) so the slide is actually perceptible and reduce
the blur range (8-26px -> 4-12px) so panes read as crisp glass, not fog.

Claude-Session: https://claude.ai/code/session_01WNP6tiqg78cXb7i23ThmQU
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR updates GlassPanes so the browser upgrades from the static SSR fallback after mount. The main changes are:

  • Adds a mounted state gate before reading reduced-motion and backdrop-filter support.
  • Keeps SSR and the first client render on the flat, non-animated fallback.
  • Re-renders after mount to apply frosted pane styles and animation state.
  • Adjusts pane blur and drift constants for crisper, more visible motion.

Confidence Score: 5/5

Safe to merge with minimal risk.

The change is localized to one visual component and preserves SSR-safe first-render behavior while using existing guarded utilities after mount.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Verified the initial render state shows data-fallback="false" and data-animating="true" on mount.
  • Validated the pane backdrop-filter includes blur(4px) saturate(1.8), and that the animation-name is fluidkit-glass-panes-slide with animation-play-state running.
  • Confirmed transforms changed between captures, proving the animation is active rather than static.
  • Collected and cataloged artifacts (two screenshots, a video, and related JSON/log files) documenting the animation state.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/components/GlassPanes.tsx Adds a post-mount re-render gate for reduced-motion and backdrop-filter feature detection, with adjusted pane drift and blur ranges.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant SSR as SSR / first client render
participant GP as GlassPanes
participant Effect as useEffect mount flag
participant Browser as Browser feature APIs
participant DOM as Pane DOM styles

SSR->>GP: "Render with mounted=false"
GP->>DOM: "data-fallback=true, animation-name=none"
Effect->>GP: setMounted(true) after mount
GP->>Browser: Read reduced-motion and backdrop-filter support
Browser-->>GP: Actual capability values
GP->>DOM: Re-render frosted styles and animation state
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant SSR as SSR / first client render
participant GP as GlassPanes
participant Effect as useEffect mount flag
participant Browser as Browser feature APIs
participant DOM as Pane DOM styles

SSR->>GP: "Render with mounted=false"
GP->>DOM: "data-fallback=true, animation-name=none"
Effect->>GP: setMounted(true) after mount
GP->>Browser: Read reduced-motion and backdrop-filter support
Browser-->>GP: Actual capability values
GP->>DOM: Re-render frosted styles and animation state
Loading

Reviews (1): Last reviewed commit: "fix(GlassPanes): upgrade off flat SSR fa..." | Re-trigger Greptile

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 6 potential issues.

Open in Devin Review

widthPct: spacing + MIN_OVERLAP_PCT + frac * OVERLAP_SPAN_PCT,
blurPx: Math.round(MIN_BLUR_PX + frac * BLUR_SPAN_PX),
driftPct: `${i % 2 === 0 ? 4 : -4}%`,
driftPct: `${i % 2 === 0 ? 13 : -13}%`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Tripled pane drift breaks the full-surface coverage guarantee, exposing gaps at default settings

The pane slide distance is tripled (driftPct changed from ±4% to ±13% at GlassPanes.tsx:136) without increasing the overlap margins that absorb it, so neighboring panes can drift apart far enough to expose the background.

Impact: At the default count of 3, and at count=2, visible gaps flash between the glass panes mid-animation, breaking the documented "every point is always behind at least one pane" guarantee.

Coverage-guarantee math showing the gap

The overlap comment at src/components/GlassPanes.tsx:78 still says "drift amplitude ≈ 4% of pane width" but the drift is now 13%. The overlap constants (MIN_OVERLAP_PCT=12, OVERLAP_SPAN_PCT=8 at lines 80-81) were sized for the old 4% drift and are now too small.

CSS translate(13%, …) moves a pane by 13% of its own width. Two adjacent panes alternate direction (even → +13%, odd → −13%), so at the animation midpoint they move apart by 0.13 × width_i + 0.13 × width_j in container-width terms.

count=3 (default):

  • spacing = 116/3 ≈ 38.7%, min pane width ≈ 50.7%
  • Worst-case gap opening = 0.13 × 50.7 + 0.13 × 55.6 ≈ 13.8% of container
  • Static overlap between adjacent panes ≈ 12%
  • Net: −1.8% → a gap of ~1.8% of the container width

count=2:

  • spacing = 58%, min pane width ≈ 70%
  • Worst-case gap opening ≈ 18.8% of container
  • Static overlap ≈ 12%
  • Net: −6.8% → a gap of ~6.8% of the container width

The old ±4% drift kept the worst-case gap opening well under the 12% minimum overlap (e.g., count=2: 0.04×70×2 = 5.6% < 12%). The fix is either to reduce the drift back toward the old value, or to increase MIN_OVERLAP_PCT to at least 2 × 0.13 × max_pane_width (roughly 26% for count=2).

Prompt for agents
The drift amplitude at line 136 was changed from ±4% to ±13% of pane width, but the overlap constants MIN_OVERLAP_PCT (line 80) and OVERLAP_SPAN_PCT (line 81) were not increased to compensate. The comment at line 78 also still references the old 4% value.

The coverage guarantee requires that the static overlap between adjacent panes exceeds the maximum drift-induced gap opening. The gap opening equals 0.13 * width_pane_i + 0.13 * width_pane_j (two adjacent panes moving in opposite directions). For count=2, this is about 18.8% of container width, far exceeding the 12% minimum overlap.

Two approaches to fix:
1. Reduce drift back to a value the current overlap can absorb. The old ±4% worked. Something up to about ±5% would be safe with the current 12% minimum overlap.
2. Increase MIN_OVERLAP_PCT to absorb the new ±13% drift. For count=2 (worst case, pane width ~70%), the overlap needs to be at least 2 * 0.13 * 78 ≈ 20.3%, so MIN_OVERLAP_PCT should be at least ~24% (accounting for jitter). This would make panes significantly wider and more overlapping.

Also update the comment at line 78 to reflect whichever drift value is chosen.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

widthPct: spacing + MIN_OVERLAP_PCT + frac * OVERLAP_SPAN_PCT,
blurPx: Math.round(MIN_BLUR_PX + frac * BLUR_SPAN_PX),
driftPct: `${i % 2 === 0 ? 4 : -4}%`,
driftPct: `${i % 2 === 0 ? 13 : -13}%`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing changelog entry for user-visible behavior changes

Several user-facing behaviors are changed — pane drift amplitude, blur range, and SSR/hydration rendering — but no CHANGELOG.md entry is added (CONTRIBUTING.md:43-44), so downstream consumers have no record of the visual and rendering changes.

Impact: Users upgrading the library won't know the glass-pane animation and SSR behavior changed.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +170 to +175
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);

const prefersReducedMotion = mounted ? rawPrefersReducedMotion : true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing test updates for changed animation and SSR behavior

The PR changes drift amplitude, blur depth range, and adds a new SSR-to-client hydration path (the mounted state gate at GlassPanes.tsx:170-175), but no tests are added or updated (CONTRIBUTING.md:41), so the new behavior is untested.

Impact: Regressions in the SSR fallback upgrade path or the new drift/blur values won't be caught by CI.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +69 to +70
const MIN_BLUR_PX = 4;
const BLUR_SPAN_PX = 8; // 4-12px

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Blur depth range halved — significantly reduced visual depth differentiation

The blur range was reduced from 8–26px (MIN_BLUR_PX=8, BLUR_SPAN_PX=18) to 4–12px (MIN_BLUR_PX=4, BLUR_SPAN_PX=8 at src/components/GlassPanes.tsx:69-70). The component's core visual premise — documented in the header comment at lines 4-6 — is that each pane gets a DIFFERENT blur strength so overlaps read as physical depth. The old 18px span gave a 3.25× ratio between the lightest and heaviest frost (8px vs 26px); the new 8px span gives only a 3× ratio (4px vs 12px) with much less absolute difference. At 4px minimum blur, the lightest-frosted pane may not read as frosted at all on high-DPI screens. This is a deliberate visual tuning choice but worth confirming it still achieves the intended depth layering effect in practice.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 182 to +188
const clampedSpeed = Math.max(speed, MIN_SPEED);
const clampedCount = Math.min(MAX_COUNT, Math.max(1, Math.round(count)));
// Guarded pure function (never throws, false in SSR) — same detector the
// engine's resolveMaterial uses, safe to call during render.
const frosted = supportsBackdropFilter();
// engine's resolveMaterial uses, safe to call during render. Gate on
// `mounted` so SSR/first render stays on the flat fallback and the client
// upgrades to frosted glass after mount.
const frosted = mounted && supportsBackdropFilter();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Extra render cycle from mounted-state hydration gate

The useState(false) + useEffect(() => setMounted(true), []) pattern at lines 170–173 guarantees a second render on every mount. During the first render, prefersReducedMotion is forced to true and frosted is forced to false, so the initial DOM has animation-name: none and no backdrop-filter. The post-mount render then flips both to their real values, causing a visual flash from flat/static to frosted/animated. This is a standard SSR hydration-safety pattern and is intentional per the comment, but consumers may notice a brief frame of the fallback appearance on client-side-only apps where SSR safety isn't needed. Other components in this repo (e.g., MeshGradient, Silk) do NOT use this pattern — they call supportsBackdropFilter() and usePrefersReducedMotion() directly during render without a mounted gate — so this creates an inconsistency across the ambient background family.

(Refers to lines 170-188)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 189 to 191
const panes = useMemo(
() => layoutPanes(colors, clampedCount, clampedSpeed),
[colors, clampedCount, clampedSpeed]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: useMemo dependency on colors array identity may cause unnecessary recalculations after mount

The useMemo at line 189–192 depends on colors by reference identity. Since the mounted-state pattern now forces a second render immediately after mount, if the parent passes a new array literal on each render (e.g., <GlassPanes colors={['red', 'blue']} />), the pane layout will be recomputed on the post-mount render even though the colors haven't semantically changed. This is a pre-existing concern (array identity instability is always a risk with inline literals), but the extra render from the mounted gate makes it fire one more time than before. The computation in layoutPanes is lightweight so the performance impact is negligible, but it's worth noting for consistency with the "byte-identical pane styles" determinism promise in the header comment (line 30–31) — the styles ARE identical, just computed twice.

(Refers to lines 189-192)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1014fa9b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

widthPct: spacing + MIN_OVERLAP_PCT + frac * OVERLAP_SPAN_PCT,
blurPx: Math.round(MIN_BLUR_PX + frac * BLUR_SPAN_PX),
driftPct: `${i % 2 === 0 ? 4 : -4}%`,
driftPct: `${i % 2 === 0 ? 13 : -13}%`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep single-pane coverage during drift

When consumers pass count={1}, this new 13% translate moves the only pane by 13% of its 128% width at the 50% keyframe; with the existing layout its interval shifts from [-16%, 112%] to about [0.64%, 128.64%], leaving a visible uncovered strip on the left edge. The component documents and tests that counts are clamped to 1–8 and should tile the full surface, so the widened drift needs matching bleed/width or a one-pane exception.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant