VectoJS UI Animation
Use this skill to turn VectoJS components into usable, accessible, and polished UI rather than just canvas drawings.
Interaction workflow
- Start with semantic components from
@vectojs/ui before writing custom controls.
- Model each interaction state: idle, hover, active, focus, loading, disabled, error, and success.
- Use canvas motion for feedback, but preserve semantic state through the projected DOM.
- Respect reduced motion. Suppress transform-heavy movement and keep essential opacity/state feedback.
- Keep overlays in the Scene overlay root; dispose or hide transient UI when its target leaves the tree.
- Test with keyboard and role-based automation, not only pointer clicks.
Read references/ui-recipes.md for patterns and snippets.
Component guidance
- Use
Input and TextArea for text entry so IME, selection, clipboard, and undo stay native.
- Use
Button, Toggle, Checkbox, Slider, Dropdown, RadioGroup, and Tabs for controls with roles.
- Use
Tooltip, Popover, ContextMenu, and Modal for transient UI; keep dismissal behavior explicit.
- On
@vectojs/ui@1.9.2+, focused editor surfaces import ContextMenu from
@vectojs/ui/context-menu to keep the application entry lean. Open it from
VectoJS pointerdown when the native pointer button is 2, using
sceneX/sceneY; Core does not emit a contextmenu event and does not expose
legacy globalX/globalY coordinates.
- As of
@vectojs/ui@2.0.0, Markdown and CodeBlock are the standalone
@vectojs/markdown package (import from '@vectojs/markdown', not
@vectojs/ui). marked loads only when you use it, so plain ui apps no
longer pay for it.
- TeX math is typeset by
@vectojs/tex, not MathJax. @vectojs/tex is a
zero-DOM vendored KaTeX parse/layout kernel plus a self-contained SVG emit
layer; mathjax-full is no longer a dependency of any package. It is imported
dynamically on the first formula, so the first one on a page renders as TeX
source for a moment and is replaced when the module resolves. The public API
is still spelled preloadMathJax() / isMathJaxReady() — those names are
deliberately retained history meaning "the math engine, whichever one it is",
and renaming them would break every consumer for cosmetics. Await
preloadMathJax() if you need the very first formula typeset synchronously.
- Use
Markdown.createStream() for token/LLM streams and
RichText.appendSpans() for span-level appends. Prefer createStream() over
hand-rolled appendMarkdown() batching: it coalesces writes per animation
frame, applies backpressure, and await close() resolves only once the final
chunk has been parsed. See vectojs-performance's references/streaming-recipes.md.
- Theming a Markdown document: pass the palette at construction or call
Markdown.setTheme(presetOrPartial). The theme property is a read-only
getter — assigning it throws at runtime (entities capture colors/fonts/sizes
at build time, so late assignment only repainted part of the document).
- Text, RichText, and Table cell text (from
@vectojs/ui) and Markdown/CodeBlock (from @vectojs/markdown) are natively selectable by default. Configure selectable or call setSelectable(); do not implement canvas clipboard or selection handles for static text.
- On
@vectojs/ui@1.9.0+ with @vectojs/core@1.8.0+, wrapped Text/RichText projections preserve logical
source across soft spaces, hard breaks, space-less CJK wraps, and Arabic/RTL
runs. Markdown lists and tables inherit the same behavior through their
RichText cells, while each standalone Table cell remains one projection.
CodeBlock shares Core's prepared source grid between per-grapheme Canvas
paint and semantic projection. This is the required path for tabs, ZWJ,
wide CJK/emoji, Arabic shaping, mixed bidi, Firefox font substitution, DPR,
zoom, rotation, mirror transforms, and non-uniform scale; a monospace font
name alone is not a geometry guarantee.
- Call
Table.layout() after changing an external Entity cell. String cells are Text entities and each logical cell owns one content projection — plus a role="gridcell" a11y hotspot (see below), so a cell is now both a selectable text surface and a keyboard target.
- Prefer
Stack/Flow composition over hand-positioning every child.
- On
@vectojs/ui@1.7.1+, use @vectojs/ui/input for Input-only code,
@vectojs/ui/text for selectable Text-only code, and @vectojs/ui/measure
for measurement-only code. On UI 1.9.2+, use @vectojs/ui/context-menu
for ContextMenu-only editor surfaces; retain the root import for
multi-component surfaces.
Keyboard & accessibility (ui 2.1.0)
Composite widgets project one role per visible child with a roving tabindex —
the whole widget is a single tab stop and arrow keys move within it. Don't
reimplement any of this:
| Component |
Child role |
Keys |
TreeView |
treeitem (+ level/expanded/selected) |
Up/Down · Right expands then enters · Left collapses then goes to parent · Home/End · Enter/Space |
Table |
row › gridcell/columnheader |
2D arrows (header is row −1) · Home/End row extremes · Ctrl+Home/Ctrl+End grid corners |
ContextMenu |
menuitem (+ haspopup/expanded) |
Up/Down wrap and skip separators + disabled · Home/End · Right opens submenu · Left returns to parent · Enter/Space · Escape |
RadioGroup |
radio |
Arrows move+select · Home/End · Space |
Tabs |
tab |
Arrows · Home/End · Space/Enter |
Those hotspots carry pointerEvents: 'none' so the component underneath keeps
the mouse (selectable cell text, tap-to-toggle, drag-to-scroll). Keyboard focus
and AT-synthesized click still work through them.
Touch: Table and TreeView drag-to-scroll 1:1 with the finger, like
ScrollView/VirtualList. TreeView fires its toggle on pointerup and only
if the pointer moved less than ~6px, so a drag doesn't expand the row it started
on.
Forced colors: read scene.forcedColors and paint with CSS system colors
(ButtonFace/ButtonText/Highlight); canvas pixels are exempt from the
browser's High Contrast remapping. Button already does this.
IME: while a composition is active, Input/TextArea suppress the selection
highlight and underline the composing range instead. The native element keeps
reporting the pre-composition selectionStart/End until commit, so painting it
would show a stale highlight wider than the underline. Don't re-add it.
Motion rules
| Scenario |
Recommended motion |
| Hover/focus |
Small color/outline/opacity changes, no layout jump |
| Press/click |
80-160 ms scale or opacity feedback |
| Overlay enter |
short fade/scale, block underlying clicks only after visible target exists |
| Streaming content |
append and reflow incrementally; avoid resetting scroll unless intended |
| Loading |
show immediate state, then progress if operation exceeds short delay |
| Error/success |
pair color with text/icon/state; do not rely only on color |
Common mistakes
- Drawing a beautiful control without
getA11yAttributes() or a native UI component.
- Animating layout so aggressively that hit boxes and projected DOM feel detached.
- Rebuilding a whole component tree for every state change.
- Importing
ContextMenu from the UI root in an otherwise focused editor entry,
which can retain rich-content dependencies and defeat the application's bundle budget.
- Hiding focus indicators on canvas controls.
- Ignoring IME and clipboard behavior by faking text entry.
- Intercepting Ctrl/Command+C while
window.getSelection()?.isCollapsed === false, which overwrites native static-text copy; likewise, do not prevent Ctrl/Command+F without a replacement find UI.
- Hand-rolling per-frame motion in
update() without telling the Scene —
invisible to the idle throttle, so the animation steps at 2 FPS or stalls
in onDemand mode. Prefer setTransition/animateTo/springTo or override
hasPendingAnimations(); on core 0.2.6+ markDirty() inside update()
also works. See vectojs-core-runtime's "Runtime gotchas".
- Relying on in-flight springs across tab switches on core ≤ 0.2.5 — the
unclamped rAF
dt made them diverge; core 0.2.6 substeps. If stuck on an
old core, re-seed positions on visibilitychange.
- Creating a new
Tooltip per hover event instead of one per target.
- Reimplementing keyboard handling that already exists.
Slider,
RadioGroup, Tabs, TreeView, Table, ContextMenu, Dropdown and
Modal all handle keydown themselves — see the keyboard table below.
- Adding a pointer handler that fights the component.
TreeView/Table
own tap-vs-drag disambiguation and drag-to-scroll; their a11y hotspots
deliberately don't capture the pointer so the component keeps it.
1---2name: vectojs-ui-animation3description: Use when creating polished VectoJS UI/UX with @vectojs/ui components, forms, overlays, hover/focus states, motion, transitions, microinteractions, or canvas-native interaction design.4---56# VectoJS UI Animation78Use this skill to turn VectoJS components into usable, accessible, and polished UI rather than just canvas drawings.910## Interaction workflow11121. Start with semantic components from `@vectojs/ui` before writing custom controls.132. Model each interaction state: idle, hover, active, focus, loading, disabled, error, and success.143. Use canvas motion for feedback, but preserve semantic state through the projected DOM.154. Respect reduced motion. Suppress transform-heavy movement and keep essential opacity/state feedback.165. Keep overlays in the Scene overlay root; dispose or hide transient UI when its target leaves the tree.176. Test with keyboard and role-based automation, not only pointer clicks.1819Read `references/ui-recipes.md` for patterns and snippets.2021## Component guidance2223- Use `Input` and `TextArea` for text entry so IME, selection, clipboard, and undo stay native.24- Use `Button`, `Toggle`, `Checkbox`, `Slider`, `Dropdown`, `RadioGroup`, and `Tabs` for controls with roles.25- Use `Tooltip`, `Popover`, `ContextMenu`, and `Modal` for transient UI; keep dismissal behavior explicit.26- On `@vectojs/ui@1.9.2+`, focused editor surfaces import `ContextMenu` from27 `@vectojs/ui/context-menu` to keep the application entry lean. Open it from28 VectoJS `pointerdown` when the native pointer button is `2`, using29 `sceneX`/`sceneY`; Core does not emit a `contextmenu` event and does not expose30 legacy `globalX`/`globalY` coordinates.31- As of `@vectojs/ui@2.0.0`, `Markdown` and `CodeBlock` are the standalone32 `@vectojs/markdown` package (import `from '@vectojs/markdown'`, not33 `@vectojs/ui`). `marked` loads only when you use it, so plain `ui` apps no34 longer pay for it.35- **TeX math is typeset by `@vectojs/tex`, not MathJax.** `@vectojs/tex` is a36 zero-DOM vendored KaTeX parse/layout kernel plus a self-contained SVG emit37 layer; `mathjax-full` is no longer a dependency of any package. It is imported38 dynamically on the first formula, so the first one on a page renders as TeX39 source for a moment and is replaced when the module resolves. The public API40 is still spelled `preloadMathJax()` / `isMathJaxReady()` — those names are41 deliberately retained history meaning "the math engine, whichever one it is",42 and renaming them would break every consumer for cosmetics. Await43 `preloadMathJax()` if you need the very first formula typeset synchronously.44- Use `Markdown.createStream()` for token/LLM streams and45 `RichText.appendSpans()` for span-level appends. Prefer `createStream()` over46 hand-rolled `appendMarkdown()` batching: it coalesces writes per animation47 frame, applies backpressure, and `await close()` resolves only once the final48 chunk has been parsed. See vectojs-performance's `references/streaming-recipes.md`.49- Theming a Markdown document: pass the palette at construction or call50 `Markdown.setTheme(presetOrPartial)`. The `theme` property is a read-only51 getter — assigning it throws at runtime (entities capture colors/fonts/sizes52 at build time, so late assignment only repainted part of the document).53- Text, RichText, and Table cell text (from `@vectojs/ui`) and `Markdown`/`CodeBlock` (from `@vectojs/markdown`) are natively selectable by default. Configure `selectable` or call `setSelectable()`; do not implement canvas clipboard or selection handles for static text.54- On `@vectojs/ui@1.9.0+` with `@vectojs/core@1.8.0+`, wrapped Text/RichText projections preserve logical55 source across soft spaces, hard breaks, space-less CJK wraps, and Arabic/RTL56 runs. Markdown lists and tables inherit the same behavior through their57 RichText cells, while each standalone Table cell remains one projection.58 `CodeBlock` shares Core's prepared source grid between per-grapheme Canvas59 paint and semantic projection. This is the required path for tabs, ZWJ,60 wide CJK/emoji, Arabic shaping, mixed bidi, Firefox font substitution, DPR,61 zoom, rotation, mirror transforms, and non-uniform scale; a monospace font62 name alone is not a geometry guarantee.63- Call `Table.layout()` after changing an external Entity cell. String cells are Text entities and each logical cell owns one content projection — plus a `role="gridcell"` a11y hotspot (see below), so a cell is now both a selectable text surface and a keyboard target.64- Prefer `Stack`/`Flow` composition over hand-positioning every child.65- On `@vectojs/ui@1.7.1+`, use `@vectojs/ui/input` for Input-only code,66 `@vectojs/ui/text` for selectable Text-only code, and `@vectojs/ui/measure`67 for measurement-only code. On UI 1.9.2+, use `@vectojs/ui/context-menu`68 for ContextMenu-only editor surfaces; retain the root import for69 multi-component surfaces.7071## Keyboard & accessibility (ui 2.1.0)7273Composite widgets project **one role per visible child** with a roving tabindex —74the whole widget is a single tab stop and arrow keys move within it. Don't75reimplement any of this:7677| Component | Child role | Keys |78| ------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |79| `TreeView` | `treeitem` (+ level/expanded/selected) | Up/Down · Right expands then enters · Left collapses then goes to parent · Home/End · Enter/Space |80| `Table` | `row` › `gridcell`/`columnheader` | 2D arrows (header is row −1) · Home/End row extremes · Ctrl+Home/Ctrl+End grid corners |81| `ContextMenu` | `menuitem` (+ haspopup/expanded) | Up/Down wrap and skip separators + disabled · Home/End · Right opens submenu · Left returns to parent · Enter/Space · Escape |82| `RadioGroup` | `radio` | Arrows move+select · Home/End · Space |83| `Tabs` | `tab` | Arrows · Home/End · Space/Enter |8485Those hotspots carry `pointerEvents: 'none'` so the component underneath keeps86the mouse (selectable cell text, tap-to-toggle, drag-to-scroll). Keyboard focus87and AT-synthesized `click` still work through them.8889**Touch**: `Table` and `TreeView` drag-to-scroll 1:1 with the finger, like90`ScrollView`/`VirtualList`. `TreeView` fires its toggle on `pointerup` and only91if the pointer moved less than ~6px, so a drag doesn't expand the row it started92on.9394**Forced colors**: read `scene.forcedColors` and paint with CSS system colors95(`ButtonFace`/`ButtonText`/`Highlight`); canvas pixels are exempt from the96browser's High Contrast remapping. `Button` already does this.9798**IME**: while a composition is active, `Input`/`TextArea` suppress the selection99highlight and underline the composing range instead. The native element keeps100reporting the pre-composition `selectionStart`/`End` until commit, so painting it101would show a stale highlight wider than the underline. Don't re-add it.102103## Motion rules104105| Scenario | Recommended motion |106| ----------------- | -------------------------------------------------------------------------- |107| Hover/focus | Small color/outline/opacity changes, no layout jump |108| Press/click | 80-160 ms scale or opacity feedback |109| Overlay enter | short fade/scale, block underlying clicks only after visible target exists |110| Streaming content | append and reflow incrementally; avoid resetting scroll unless intended |111| Loading | show immediate state, then progress if operation exceeds short delay |112| Error/success | pair color with text/icon/state; do not rely only on color |113114## Common mistakes115116- Drawing a beautiful control without `getA11yAttributes()` or a native UI component.117- Animating layout so aggressively that hit boxes and projected DOM feel detached.118- Rebuilding a whole component tree for every state change.119- Importing `ContextMenu` from the UI root in an otherwise focused editor entry,120 which can retain rich-content dependencies and defeat the application's bundle budget.121- Hiding focus indicators on canvas controls.122- Ignoring IME and clipboard behavior by faking text entry.123- Intercepting Ctrl/Command+C while `window.getSelection()?.isCollapsed === false`, which overwrites native static-text copy; likewise, do not prevent Ctrl/Command+F without a replacement find UI.124- Hand-rolling per-frame motion in `update()` without telling the Scene —125 invisible to the idle throttle, so the animation steps at 2 FPS or stalls126 in onDemand mode. Prefer `setTransition`/`animateTo`/`springTo` or override127 `hasPendingAnimations()`; on core 0.2.6+ `markDirty()` inside `update()`128 also works. See vectojs-core-runtime's "Runtime gotchas".129- Relying on in-flight springs across tab switches on core ≤ 0.2.5 — the130 unclamped rAF `dt` made them diverge; core 0.2.6 substeps. If stuck on an131 old core, re-seed positions on `visibilitychange`.132- Creating a new `Tooltip` per hover event instead of one per target.133- **Reimplementing keyboard handling that already exists.** `Slider`,134 `RadioGroup`, `Tabs`, `TreeView`, `Table`, `ContextMenu`, `Dropdown` and135 `Modal` all handle `keydown` themselves — see the keyboard table below.136- **Adding a pointer handler that fights the component.** `TreeView`/`Table`137 own tap-vs-drag disambiguation and drag-to-scroll; their a11y hotspots138 deliberately don't capture the pointer so the component keeps it.