VectoJS Responsive Layout
Use this skill when a VectoJS UI must adapt to viewport size, browser zoom, density, content length, or resizable panels.
Layout workflow
- Define layout state in logical CSS pixels, not backing-store pixels.
- Resize the
Scene from the containing element, not blindly from window, when embedding in an app shell.
- Compose with
Stack, Flow, Card, ScrollView, VirtualList, TreeView, and resizable panels.
- After direct child size changes, call the parent layout method and mark the scene dirty.
- Keep one scroll owner per region; avoid nested wheel handlers without clear boundaries.
- Use role labels and semantic regions even for canvas-rendered layout containers.
Read references/layout-recipes.md for copyable patterns.
For OS/browser/zoom/DPR consistency and text-selection alignment (embedded
scene.resize() bridge, Firefox Range recalibration, web-font race, the
DPR-1-vs-2 test matrix), read references/cross-environment.md.
Design rules
- Use
scene.resize(width, height) for logical layout dimensions.
- Let VectoJS/Canvas handle high-DPI backing stores; do not multiply layout coordinates by DPR.
- Make breakpoint decisions from available logical width/height.
- Prefer reflowing existing entities to destroying and recreating the full tree.
- Reserve virtualization for long lists/tables/trees; do not render thousands of offscreen rows as regular children.
- Treat browser zoom as a layout-input multiplier only when product requirements explicitly need density changes beyond normal CSS pixel behavior.
Common mistakes
| Mistake |
Correction |
Reading canvas.width for layout |
Use container CSS size or scene.width / scene.height. |
| Recreating every component on resize |
Reposition/reconfigure existing entities, then scene.markDirty(). |
| ScrollView inside another wheel-capturing region |
Give each wheel gesture one owner or define escape behavior. |
| Fixed desktop-only coordinates |
Add breakpoint functions and test narrow, wide, and zoomed layouts. |
| Changing child size without relayout |
Call stack.layout() / equivalent and mark dirty. |
Hand-syncing content.width = panel.width after every PanelGroup drag/resize |
Panel.setContent(content, fit?) / Card.setContent(content, fit?) (>= @vectojs/ui@1.11.0) track the container's box automatically, fit: true by default. Pass fit: false for self-sizing content (bare Text with no maxWidth) — the default would overwrite its self-computed box every frame; wrap it in a Stack first if it needs to fill the viewport instead. |
| Growing ScrollView content without re-measuring |
scrollView.add() measures automatically, but mutating an existing child's size needs scrollView.updateContentSize() or the max-scroll clamp goes stale. |
Guessing estimatedRowHeight for fixed-height VirtualList rows |
Set it to the exact row height — the estimate only exists for variable rows (measured heights are cached per index); setItems() resets scroll and that cache. |
Passing canvas.width/canvas.height into a layout function |
Those are the DPR-scaled backing store (2× at retina). Pass window.innerWidth/scene.width — the logical size — or panels double in width. |
Expecting Tabs to stay legible with many tabs |
Tabs (>= @vectojs/ui@1.1.3) keeps a fixed tabWidth (floor minTabWidth) and scrolls horizontally; pass closable: true + onClose for per-tab × close. Since 1.9.4 surplus bar width stays empty (never stretches past tabWidth). |
Wanting no tab bar while only one tab exists (Vim showtabline=1) |
Tabs (>= @vectojs/ui@1.9.5) autoHideTabBar: true — bar + hit region vanish below two tabs, content takes full height; read the live effectiveTabBarHeight getter (not tabHeight) when laying out siblings around the bar. |
Drag handler reading localX deltas from the dragged handle |
PanelResizeHandle (>= 1.1.3) uses sceneX/sceneY — a coordinate space that does not move with the handle. Any custom drag must do the same or it lags the cursor. |
| Selection highlights drift after zoom (Firefox), canvas looks fine |
The app owns sizing but never calls scene.resize() — it is the Range-metric recalibration hook. Bridge the container via ResizeObserver (references/cross-environment.md). |
| Hit/selection tests pass headless, fail on real laptops |
Headless runs DPR 1; real machines are DPR 2 — run pointer/selection tests at deviceScaleFactor: 2 too. Offset proportional to distance from origin ⇒ DPR bug. |
| Text renders in a different font than it was measured with |
Web-font race: construct text after await document.fonts.ready, re-measure from document.fonts.onloadingdone for lazy fonts. |
Scroll owners and RTL
One scroll owner per region. ScrollView, VirtualList, TreeView, and a
virtualized Table (viewportHeight) each own their region's scroll. Nesting two
owners, or adding your own wheel/drag handler on top of one, produces
double-scrolling. All four also drag-to-scroll on touch (the content follows
the finger 1:1), so don't add a touch handler either.
Sizing notes that bite:
VirtualList needs a sane estimatedRowHeight; a wrong estimate shows as a
scrollbar that jumps as real heights are measured.
Table virtualizes only when you give it viewportHeight. Without it the
whole grid mounts.
TreeView fires its toggle on pointerup and only within ~6px of the
pointerdown, so a drag scrolls instead of expanding a row.
RTL is a layout concern, not just a text one. An RTL paragraph right-aligns,
and selection anchors at the visual origin. For the a11y layer, set
readingDirection: 'rtl' on the Scene so tab order reverses inline within
each visual row — tab order follows where things are drawn, not the order they
were added, so an RTL UI that skips this tabs in the wrong direction.
Verification
Check at least:
- narrow mobile width;
- desktop width;
- browser zoom 125% and 150%;
- keyboard navigation/focus order;
- an RTL pass (
readingDirection: 'rtl') if the app is ever localized;
- role-based automation for interactive controls.
1---2name: vectojs-responsive-layout3description: Use when designing adaptive VectoJS canvas layouts with browser resize or zoom, Stack, Flow, Card, ScrollView, VirtualList, responsive panels, dashboards, forms, or layout reflow.4---56# VectoJS Responsive Layout78Use this skill when a VectoJS UI must adapt to viewport size, browser zoom, density, content length, or resizable panels.910## Layout workflow11121. Define layout state in logical CSS pixels, not backing-store pixels.132. Resize the `Scene` from the containing element, not blindly from `window`, when embedding in an app shell.143. Compose with `Stack`, `Flow`, `Card`, `ScrollView`, `VirtualList`, `TreeView`, and resizable panels.154. After direct child size changes, call the parent layout method and mark the scene dirty.165. Keep one scroll owner per region; avoid nested wheel handlers without clear boundaries.176. Use role labels and semantic regions even for canvas-rendered layout containers.1819Read `references/layout-recipes.md` for copyable patterns.20For OS/browser/zoom/DPR consistency and text-selection alignment (embedded21`scene.resize()` bridge, Firefox Range recalibration, web-font race, the22DPR-1-vs-2 test matrix), read `references/cross-environment.md`.2324## Design rules2526- Use `scene.resize(width, height)` for logical layout dimensions.27- Let VectoJS/Canvas handle high-DPI backing stores; do not multiply layout coordinates by DPR.28- Make breakpoint decisions from available logical width/height.29- Prefer reflowing existing entities to destroying and recreating the full tree.30- Reserve virtualization for long lists/tables/trees; do not render thousands of offscreen rows as regular children.31- Treat browser zoom as a layout-input multiplier only when product requirements explicitly need density changes beyond normal CSS pixel behavior.3233## Common mistakes3435| Mistake | Correction |36| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |37| Reading `canvas.width` for layout | Use container CSS size or `scene.width` / `scene.height`. |38| Recreating every component on resize | Reposition/reconfigure existing entities, then `scene.markDirty()`. |39| ScrollView inside another wheel-capturing region | Give each wheel gesture one owner or define escape behavior. |40| Fixed desktop-only coordinates | Add breakpoint functions and test narrow, wide, and zoomed layouts. |41| Changing child size without relayout | Call `stack.layout()` / equivalent and mark dirty. |42| Hand-syncing `content.width = panel.width` after every `PanelGroup` drag/resize | `Panel.setContent(content, fit?)` / `Card.setContent(content, fit?)` (>= `@vectojs/ui@1.11.0`) track the container's box automatically, `fit: true` by default. Pass `fit: false` for self-sizing content (bare `Text` with no `maxWidth`) — the default would overwrite its self-computed box every frame; wrap it in a `Stack` first if it needs to fill the viewport instead. |43| Growing ScrollView content without re-measuring | `scrollView.add()` measures automatically, but mutating an existing child's size needs `scrollView.updateContentSize()` or the max-scroll clamp goes stale. |44| Guessing `estimatedRowHeight` for fixed-height `VirtualList` rows | Set it to the exact row height — the estimate only exists for variable rows (measured heights are cached per index); `setItems()` resets scroll and that cache. |45| Passing `canvas.width`/`canvas.height` into a layout function | Those are the DPR-scaled backing store (2× at retina). Pass `window.innerWidth`/`scene.width` — the logical size — or panels double in width. |46| Expecting `Tabs` to stay legible with many tabs | `Tabs` (>= `@vectojs/ui@1.1.3`) keeps a fixed `tabWidth` (floor `minTabWidth`) and scrolls horizontally; pass `closable: true` + `onClose` for per-tab × close. Since 1.9.4 surplus bar width stays empty (never stretches past `tabWidth`). |47| Wanting no tab bar while only one tab exists (Vim `showtabline=1`) | `Tabs` (>= `@vectojs/ui@1.9.5`) `autoHideTabBar: true` — bar + hit region vanish below two tabs, content takes full height; read the live `effectiveTabBarHeight` getter (not `tabHeight`) when laying out siblings around the bar. |48| Drag handler reading `localX` deltas from the dragged handle | `PanelResizeHandle` (>= `1.1.3`) uses `sceneX`/`sceneY` — a coordinate space that does not move with the handle. Any custom drag must do the same or it lags the cursor. |49| Selection highlights drift after zoom (Firefox), canvas looks fine | The app owns sizing but never calls `scene.resize()` — it is the Range-metric recalibration hook. Bridge the container via ResizeObserver (`references/cross-environment.md`). |50| Hit/selection tests pass headless, fail on real laptops | Headless runs DPR 1; real machines are DPR 2 — run pointer/selection tests at `deviceScaleFactor: 2` too. Offset proportional to distance from origin ⇒ DPR bug. |51| Text renders in a different font than it was measured with | Web-font race: construct text after `await document.fonts.ready`, re-measure from `document.fonts.onloadingdone` for lazy fonts. |5253## Scroll owners and RTL5455**One scroll owner per region.** `ScrollView`, `VirtualList`, `TreeView`, and a56virtualized `Table` (`viewportHeight`) each own their region's scroll. Nesting two57owners, or adding your own wheel/drag handler on top of one, produces58double-scrolling. All four also **drag-to-scroll on touch** (the content follows59the finger 1:1), so don't add a touch handler either.6061Sizing notes that bite:6263- `VirtualList` needs a sane `estimatedRowHeight`; a wrong estimate shows as a64 scrollbar that jumps as real heights are measured.65- `Table` virtualizes only when you give it `viewportHeight`. Without it the66 whole grid mounts.67- `TreeView` fires its toggle on `pointerup` and only within ~6px of the68 pointerdown, so a drag scrolls instead of expanding a row.6970**RTL is a layout concern, not just a text one.** An RTL paragraph right-aligns,71and selection anchors at the visual origin. For the a11y layer, set72`readingDirection: 'rtl'` on the Scene so **tab order** reverses inline within73each visual row — tab order follows where things are drawn, not the order they74were added, so an RTL UI that skips this tabs in the wrong direction.7576## Verification7778Check at least:7980- narrow mobile width;81- desktop width;82- browser zoom 125% and 150%;83- keyboard navigation/focus order;84- an RTL pass (`readingDirection: 'rtl'`) if the app is ever localized;85- role-based automation for interactive controls.