🐛 Drawer/Dock Layout Snap on Close

Date: 2026-04-12
Severity: High (visual regression)
Status: ✅ Fixed
Components: GameDrawer, GameDock, gameDrawer.svelte.ts, play/[id]/+page.svelte

Symptoms

  • Empty space / ghost gap flashes briefly after closing the game drawer
  • Dock bar snaps rightward momentarily before settling at full width
  • Layout “jumps” visible on desktop when repeatedly opening/closing drawer

Root Cause

The --drawer-w CSS custom property was being removed instantly when the drawer closed, but multiple CSS consumers (.play-root, .game-dock, .pool-iframe) were still mid-transition using transition: right 0.22s:

  1. GameDrawer.svelte $effect cleanup called removeProperty('--drawer-w') synchronously when gameDrawer.openfalse
  2. .play-root.drawer-open used right: var(--drawer-w, 24rem) with a 220ms transition
  3. When --drawer-w vanished mid-transition, CSS fell back to 24rem (the hardcoded fallback) → visible snap
  4. Same issue affected GameDock (.game-dock--drawer-open) and ViewportPool iframe sizing

Secondary issue: dockEl in GameDock.svelte was declared as plain let instead of $state, risking null when bind:this assignment raced against onMount + transition:fly.

Fix

1. Centralized --drawer-w lifecycle in gameDrawer.svelte.ts

  • openDrawer(): Cancels any pending cleanup timer, pre-seeds --drawer-w if missing
  • closeDrawer(): Defers removeProperty('--drawer-w') by 250ms (> transition duration 220ms) so CSS transitions animate using the live pixel value, never hitting the 24rem fallback
  • reset(): Immediately removes the var (no transition context)
  • Timer is tracked via drawerWidthCleanupTimer — cancelled if drawer re-opens before cleanup fires

2. Removed premature cleanup from GameDrawer.svelte $effect

The ResizeObserver effect no longer calls removeProperty('--drawer-w') in its cleanup. The store is the single owner of the CSS var lifecycle.

3. Fixed dockEl reactivity in GameDock.svelte

Changed let dockEl: HTMLElement | null = null$state(null) for proper Svelte 5 bind:this reactivity.

Files Changed

FileChange
src/lib/stores/gameDrawer.svelte.tsAdded TRANSITION_DURATION_MS, DEFAULT_DRAWER_WIDTH, deferred cleanup in closeDrawer(), cancel-on-reopen in openDrawer(), immediate cleanup in reset()
src/lib/components/games/GameDrawer.svelteRemoved removeProperty('--drawer-w') from $effect cleanup
src/lib/components/games/GameDock.sveltedockEl$state(null)

Consumers of --drawer-w

ConsumerFallbackGuarded by .drawer-open?Status
.play-root.drawer-open24rem✅ YesFixed (deferred removal)
.game-dock--drawer-open24rem✅ YesFixed (deferred removal)
ViewportPool .pool-active0px✅ Yes (:global(.drawer-open))Was safe
NativeGameHost .pool-native0px✅ Yes (:global(.drawer-open))Was safe
GameDrawer itself24remN/A (removed with {#if})Converges on mount

Verification

  • svelte-check: 0 errors ✅
  • vite build: Success ✅

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send