Capability
texuli/motion · in development · on npm from 0.6.0
Motion that respects the reader
One small runtime moves every Texuli surface: overlays enter and leave, lists glide, a card grows into its detail view, timelines play on one clock, springs settle. The browser does the interpolation through the Web Animations API. Reduced motion (prefers-reduced-motion) is instant by construction, and the settings below can force reduced motion on every demo. The motion system is in development and on npm from 0.6.0, a pre-1.0 release; 0.5.0 and earlier are without it.
animateWeb Animations, one runpresenceenter and exit, reversibleflipreorder, insert, removesharedone element, two placestimelineplay, pause, seek, reversestaggeroffsets, capped at 1 sspringa pure solver, linear() curveslevelsfull, subtle, none
Motion level and reduced motion
Every demo on this page reads these two settings through MotionProvider or the runtime options. Lower the level, or turn on reduced motion, and watch the same code commit its end state at once.
Your system: checking
fullEvery preset plays at its token duration.subtleDurations shrink to 60%, and travel and scale are dropped: a move commits at once, a fade stays.noneNothing animates: every change commits at once.
Not played yet
fast | base | slow | slower |
|---|---|---|---|
| 150 ms | 200 ms | 300 ms | 500 ms |
Every preset
The five enter and exit presets of the closed vocabulary, the same names a component prop or an agent blueprint uses, plus the edge slides sheets and drawers play. Replay any of them at any duration step; each card shows the keyframes its preset resolves to.
-
fadeKeyframes load with the runtime.
-
riseKeyframes load with the runtime.
-
scaleKeyframes load with the runtime.
-
slide-startKeyframes load with the runtime.
-
slide-endKeyframes load with the runtime.
-
edge slide, leftKeyframes load with the runtime.
-
edge slide, rightKeyframes load with the runtime.
-
edge slide, topKeyframes load with the runtime.
-
edge slide, bottomKeyframes load with the runtime.
Level full, reduced motion follows your system Change the settings
Presence: overlays that enter and leave
Dialog, Sheet and the Toaster animate through the shared runtime. Pick the presets: they are the same closed names an agent can use. A closed overlay gives focus back and lifts inert at once; only the paint waits for the exit.
motion={{ enter: 'scale', exit: 'fade', duration: 'base' }} Level full, reduced motion follows your system Change the settings
Every component that moves
Tooltips, popovers, hover cards and the four menus scale in; the alert dialog scales over its backdrop; drawers, detail panels and responsive modals slide from their edge; the accordion opens its height and the tabs indicator glides. All of them follow the settings above. The table lists every package source that animates, checked against the code.
Tooltip
Popover
Hover card
Platform lead · [email protected]
Dropdown menu and submenu
Context menu
Menubar
Alert dialog
Drawer
Detail panel
Responsive modal
Accordion
Tabs
The notes of the fictional release.
The owners of the fictional release.
The dates of the fictional release.
Level full, reduced motion follows your system Change the settings
| Component | Package | What moves | See it |
|---|---|---|---|
| Dialog | texuli | Panel scale in and out over a fading backdrop, base | On this page |
| AlertDialog | texuli | Panel scale in and out over a fading backdrop, base | On this page |
| Sheet | texuli | Slides from its edge the whole way (rise), base | On this page |
| Sheet overlay | texuli | Backdrop fade in and out | On this page |
| Drawer | texuli | Slides from its edge to its snap point, base | On this page |
| DetailPanel | texuli | Slides from its edge, base | On this page |
| ResponsiveModal | texuli | Dialog or bottom drawer, slides from its edge, base | On this page |
| Popover | texuli | Scale in and out, fast | On this page |
| HoverCard | texuli | Scale in, fade out, fast | On this page |
| Tooltip | texuli | Scale in, fade out, fast | On this page |
| DropdownMenu | texuli | Scale in, fade out, fast | On this page |
| Submenu | texuli | Scale in, fade out, fast | On this page |
| ContextMenu | texuli | Scale in, fade out, fast | On this page |
| Menubar | texuli | Scale in, fade out, fast | On this page |
| Toaster | texuli | Rise in, fade out; the stack glides (layout), base | On this page |
| Tabs | texuli | The indicator glides to the new tab (layout), base | On this page |
| Accordion | texuli | Panel height opens and closes (layout), base | On this page |
| Dashboard | @texuli/data | Widgets glide to their new cell after a change (FLIP), in development | /dashboards-page/ |
| DataGrid | @texuli/data | Rows glide on sort, insert and remove (FLIP), base | Its showcase page is planned |
| Board | @texuli/data | Cards glide on a committed move (FLIP), base | On this page |
| InteractivePlot (every interactive chart) | @texuli/charts | Follows the MotionProvider reduced-motion setting and the OS preference (data-reduced-motion) | On this page |
| Board, DataGrid and Dashboard list motion | @texuli/data | Loads texuli/motion list motion only once a change can animate | On this page |
| SchemaForm | @texuli/forms | Each new step fades in, fast | /forms-page/#flow |
| PageEditor blocks | @texuli/pages | Blocks glide on insert, remove and move (FLIP), fast | /solutions/documents/ |
| PageEditor | @texuli/pages | Takes the motion prop and passes it to its block list | /solutions/documents/ |
| Editor context | @texuli/pages | Shares the editor’s motion prop with its rows | /solutions/documents/ |
| CollabEditor | @texuli/pages | The same block motion in collaborative editing | /solutions/documents/ |
| Canvas zoom and fit | @texuli/canvas | Zoom and fit glide through the runtime | /solutions/canvas/ |
| Player motion settings | @texuli/player | The level a blueprint or host sets | On this page |
| Blueprint node motion | @texuli/player | Enter, exit, layout and stagger declared as data | On this page |
| Slide transitions and fragments | @texuli/slides | Every transition and fragment effect at the deck’s level | On this page |
| Stage motion (static half) | @texuli/slides | Reads fragments before a step change and loads the runtime half | On this page |
Layout: lists and boards that glide
FLIP measures every item before a change and plays each one from where it was. The list below uses the same seam as the Board from @texuli/data, so a reorder, an insert or a committed move glides instead of jumping.
A keyed list
- Write launch notes
- Refresh pricing page
- Check status page
- Send May invoices
Level full, reduced motion follows your system Change the settings
A board
Release board
Focus a card handle and press Space or Enter to pick it up. Use the arrow keys to choose a position, then Space or Enter to drop. Press Escape to cancel. Or open Move on a card and choose a column and position from the menu.
To do
3 cardsDoing
1 cardDone
1 cardTimelines and stagger
Declared steps on one clock: play, pause, seek, reverse. Stagger spreads the tiles from the start, the center or the end, and the whole spread is held to 1,000 ms however many items there are.
Level full, reduced motion follows your system Not played yet Change the settings
Springs
Three spring tokens solved by the owned damped-oscillator solver. Each curve is precomputed as a CSS linear() easing, so the browser plays it on its compositor; the plot below is drawn from the same solver.
springSamples in @texuli/core/motion/spring, the solver that
precomputed each token's linear() easing.- gentle
--tx-motion-spring-gentle - snappy
--tx-motion-spring-snappy - bouncy
--tx-motion-spring-bouncy
Level full, reduced motion follows your system Not played yet Change the settings
Spring lab
Any spring, solved live. Move stiffness, damping, mass and the initial velocity and watch the curve, the damping ratio, the settle time and the linear() easing the browser plays. Every spring settles within 1,000 ms; parameters outside the solver’s ranges are clamped.
- Damping ratio
- 1 (critically damped)
- Settles in
- 718 ms
- Peak
- 1
The easing the browser plays: linear(0, 0.0635, 0.1965, 0.3455, 0.4849, 0.6045, 0.7019, 0.7785, 0.8373, 0.8816, 0.9146, 0.9388, 0.9564, 0.9691, 0.9782, 0.9846, 0.9892,…
Level full, reduced motion follows your system Change the settings
Tokens explorer
Five durations, five easings and three springs, as data in @texuli/tokens/motion and as --tx-motion-* custom properties. Play any token to see it; under reduced motion every duration is 0 ms.
Pick a token to play it
| Step | Default | Custom property | Play |
|---|---|---|---|
instant | 0 ms | --tx-motion-instant | |
fast | 150 ms | --tx-motion-fast | |
base | 200 ms | --tx-motion-base | |
slow | 300 ms | --tx-motion-slow | |
slower | 500 ms | --tx-motion-slower |
| Name | Value | Custom property | Play |
|---|---|---|---|
standard | cubic-bezier(0.4, 0, 0.2, 1) | --tx-motion-ease-standard | |
emphasized | cubic-bezier(0.2, 0, 0, 1) | --tx-motion-ease-emphasized | |
enter | cubic-bezier(0, 0, 0.2, 1) | --tx-motion-ease-enter | |
exit | cubic-bezier(0.4, 0, 1, 1) | --tx-motion-ease-exit | |
linear | linear | --tx-motion-ease-linear |
| Name | Parameters | Custom properties | Play |
|---|---|---|---|
gentle | 120 / 20 / 1, 648 ms | --tx-motion-spring-gentle --tx-motion-spring-gentle-duration | |
snappy | 400 / 30 / 1, 453 ms | --tx-motion-spring-snappy --tx-motion-spring-snappy-duration | |
bouncy | 380 / 18 / 1, 844 ms | --tx-motion-spring-bouncy --tx-motion-spring-bouncy-duration |
Level full, reduced motion follows your system Change the settings
Deck motion: transitions, fragments and levels
Slides share the same three levels. Every slide transition and every fragment effect, on the slides stage, at the level chosen in the settings: subtle shortens everything and plays zoom as a fade, none cuts, and reduced motion turns every transition into a short fade. Press the right arrow on the stage to step through the fragments.
Deck motion level: full. Change it in the settings at the top of the page.
Loading the slides stage…
| Transition | full | subtle | none | reduced motion |
|---|---|---|---|---|
none | cut | cut | cut | cut |
fade | fade, 300 ms | fade, 180 ms | cut | fade, 150 ms |
fade-up | fade-up, 300 ms | fade-up, 180 ms | cut | fade, 150 ms |
slide | slide, 400 ms | slide, 240 ms | cut | fade, 150 ms |
zoom | zoom, 350 ms | fade, 180 ms | cut | fade, 150 ms |
morph | morph, 400 ms | morph, 240 ms | cut | fade, 150 ms |
Motion as data: an agent declares it, the player paints it
A blueprint names presets and token steps, never keyframes, CSS or milliseconds. The player checks the vocabulary, plays the checked copy through the same runtime, and paints a refused motion without animating it.
The blueprint
A node may carry motion with enter and exit (fade, rise, scale, slide-start, slide-end), layout, stagger (none, fast, base) and duration (fast, base, slow); the envelope may set a level. Spec 0.6.0 ships in the 0.6.0 release of @texuli/spec.
{
"format": "texuli-blueprint",
"version": 1,
"spec": "0.6.0",
"id": "motion-release-brief",
"catalog": "0000000000000000000000000000000000000000000000000000000000000000",
"status": "draft",
"createdAt": "2026-10-28T09:00:00Z",
"updatedAt": "2026-10-28T09:00:00Z",
"generator": {
"kind": "agent",
"name": "Harbor assistant",
"model": "example-model"
},
"state": {
"details": {
"type": "boolean",
"initial": false
}
},
"motion": {
"level": "full"
},
"tree": {
"id": "layout-page",
"props": {
"title": "Release brief, week 44",
"description": "Harbor Notes ships on Thursday. Turn on the details to see what is left."
},
"children": [
{
"id": "control-toggle",
"props": {
"label": "Show the details",
"state": "details"
}
},
{
"id": "layout-grid",
"props": {
"columns": 3
},
"motion": {
"stagger": "base"
},
"children": [
{
"id": "layout-card",
"props": {
"title": "Docs"
},
"when": {
"op": "eq",
"left": {
"op": "var",
"name": "details"
},
"right": {
"op": "num",
"value": 1
}
},
"motion": {
"enter": "rise",
"exit": "fade",
"duration": "base"
},
"children": [
{
"id": "blocks-text",
"props": {
"block": {
"type": "text",
"markdown": "Two guides left to review."
}
}
}
]
},
{
"id": "layout-card",
"props": {
"title": "Checks"
},
"when": {
"op": "eq",
"left": {
"op": "var",
"name": "details"
},
"right": {
"op": "num",
"value": 1
}
},
"motion": {
"enter": "rise",
"exit": "fade",
"duration": "base"
},
"children": [
{
"id": "blocks-text",
"props": {
"block": {
"type": "text",
"markdown": "All green on the release branch."
}
}
}
]
},
{
"id": "layout-card",
"props": {
"title": "Launch post"
},
"when": {
"op": "eq",
"left": {
"op": "var",
"name": "details"
},
"right": {
"op": "num",
"value": 1
}
},
"motion": {
"enter": "rise",
"exit": "fade",
"duration": "base"
},
"children": [
{
"id": "blocks-text",
"props": {
"block": {
"type": "text",
"markdown": "Draft shared with the crew."
}
}
}
]
}
]
},
{
"id": "layout-card",
"props": {
"title": "Risk review"
},
"when": {
"op": "eq",
"left": {
"op": "var",
"name": "details"
},
"right": {
"op": "num",
"value": 1
}
},
"motion": {
"enter": "scale",
"exit": "fade",
"duration": "slow",
"layout": true
},
"children": [
{
"id": "blocks-text",
"props": {
"block": {
"type": "text",
"markdown": "The status page at status.example.com needs one more check."
}
}
}
]
}
]
}
}The refused node
{
"id": "layout-card",
"props": {
"title": "Risk review"
},
"when": {
"op": "eq",
"left": {
"op": "var",
"name": "details"
},
"right": {
"op": "num",
"value": 1
}
},
"motion": {
"enter": "spin",
"keyframes": [
{
"transform": "rotate(720deg)"
}
]
},
"children": [
{
"id": "blocks-text",
"props": {
"block": {
"type": "text",
"markdown": "This card paints without its motion."
}
}
}
]
}Painted by the player
Motion blueprint (untrusted)
Untrusted content DraftNo errors: the motion is inside the vocabulary, so the player plays it.
Level full, reduced motion follows your system Change the settings
Every export and every token
The complete public surface of the motion system, read from the packages when this page was built, so nothing is missing: the runtime texels, the spring solver, the motion tokens with their custom properties, and the component contract.
@texuli/core/motionThe runtime texels: 39 exports
DEFAULT_MOTION_DURATIONDEFAULT_MOTION_EASINGDEFAULT_SPRINGDEFAULT_SPRING_SAMPLESMAX_MOTION_MSMAX_SPRING_MSMAX_SPRING_SAMPLESMAX_STAGGER_SPANMAX_TIMELINE_STEPSMOTION_LEVELSREDUCED_FADE_MSSUBTLE_DURATION_SCALEanimatecreateFlipcreateMotionPresencecreateReducedMotionSourcecreateRuncreateSharedElementsdampingRatioflipTransformisMotionLevellowerMotionLevelmotionDurationnormalizeSpringparallelresolveMotionresolveMotionLevelretargetVelocitysequencespringEasingspringKeyframesspringSamplesspringSettlespringStatestaggerstartAnimationstartScopedViewTransitiontimelinewithoutTravel
@texuli/core/motion/springThe spring solver: 12 exports
DEFAULT_SPRINGDEFAULT_SPRING_SAMPLESMAX_SPRING_MSMAX_SPRING_SAMPLESdampingRationormalizeSpringretargetVelocityspringEasingspringKeyframesspringSamplesspringSettlespringState
@texuli/tokens/motionThe motion tokens: 13 exports
MOTION_DURATIONSMOTION_DURATION_STEPSMOTION_EASINGSMOTION_EASING_NAMESMOTION_SPRINGSMOTION_SPRING_NAMESMOTION_SPRING_SAMPLESMOTION_VARIABLE_NAMESmotionDurationVarmotionEasingVarmotionSpringVarsmotionVariablesslowerDuration
texuli/motionThe component contract and seams: 18 exports
DEFAULT_MOTION_SETTINGSMOTION_DURATION_NAMESMOTION_LEVEL_NAMESMOTION_PRESETSMotionProvidercreateIndicatorMotioncreateListMotioncreateOverlayMotioncreatePresenceMotioncreateSizeMotiongetMotionSettingsisMotionLevelNameisMotionPresetloadMotionRuntimelowerLevelmotionRuntimeresolveComponentMotionsetMotionSettings
| Custom property | Default |
|---|---|
--tx-motion-instant | 0ms |
--tx-motion-fast | 150ms |
--tx-motion-base | 200ms |
--tx-motion-slow | 300ms |
--tx-motion-slower | 500ms |
--tx-motion-ease-standard | cubic-bezier(0.4, 0, 0.2, 1) |
--tx-motion-ease-emphasized | cubic-bezier(0.2, 0, 0, 1) |
--tx-motion-ease-enter | cubic-bezier(0, 0, 0.2, 1) |
--tx-motion-ease-exit | cubic-bezier(0.4, 0, 1, 1) |
--tx-motion-ease-linear | linear |
--tx-motion-spring-gentle | linear(0, 0.0228, 0.0795, 0.1561, 0.2426, 0.3318, 0.419, 0.5013, 0.5767, 0.6444, 0.7041… |
--tx-motion-spring-gentle-duration | 648ms |
--tx-motion-spring-snappy | linear(0, 0.0368, 0.1267, 0.2447, 0.3726, 0.4983, 0.6137, 0.7148, 0.7995, 0.8679, 0.921… |
--tx-motion-spring-snappy-duration | 453ms |
--tx-motion-spring-bouncy | linear(0, 0.1177, 0.3817, 0.6779, 0.9294, 1.0989, 1.1811, 1.1921, 1.1571, 1.1015, 1.045… |
--tx-motion-spring-bouncy-duration | 844ms |
The rules every animation keeps
They hold by construction in the runtime, not by care in each component.
Reduced motion is instant
Under prefers-reduced-motion, a provider override or level none, every texel commits the end state at once: no frames run, the finished promise resolves "instant" and callbacks fire in order.
Nothing animates on hydration
The server paints the end state. Content present on the first render never animates in; animations start only after mount.
No lasting inline style
Every animation plays with fill "none"; the final state lives in classes, attributes or props. That keeps the strict style CSP of this site intact.
The compositor interpolates
Everything plays through the Web Animations API; no requestAnimationFrame loop writes styles. Springs become linear() easings.
Focus never waits
A closing overlay returns focus, lifts inert and leaves the layer stack at once; only its paint waits for the exit, at most 1.2 s.
Hostile input is clamped
Durations and delays are at most 1,000 ms; spring parameters, sample counts, FLIP deltas and list sizes have fixed ranges. Promises never reject.
In code
import { MotionProvider } from 'texuli/motion';
import { Dialog } from 'texuli/dialog';
<MotionProvider level="subtle">
<Dialog title="Edit crew" motion={{ enter: 'rise', exit: 'fade', duration: 'slow' }} />
</MotionProvider>import { animate, createFlip, stagger, timeline } from '@texuli/core/motion';
const run = animate(card, [{ opacity: 0 }, { opacity: 1 }], { duration: 150 });
await run.finished; // "finished", or "instant" under reduced motionOne API contract
Motion is one prop in a contract every exported component follows, so a styled component, a primitive and an agent blueprint behave the same way.
0.5.0 brought bind:ref and rest attributes to about 40 components and made the inputs and 14 primitive parts compose a caller's handlers. 0.6.0 brought the rest: the remaining parts, the Field rule, and a contract test that derives every component from the package exports, so a new export without a case fails.
Rest attributes and ref
Attributes a component does not consume (id, aria-*, data-*, class, style, handlers) land on its root element, and bind:ref binds that same element. Form controls with a field wrapper send aria-*, data-* and handlers to the control and layout hooks to the wrapper.
<Dialog bind:ref={panel} data-testid="crew" aria-describedby="crew-hint" />Handlers compose: the caller first
A handler you pass never replaces the component's own. It runs first; for actions (click, keydown, pointerdown) event.preventDefault() skips the built-in behavior, and bookkeeping (focus, blur, scroll) always runs.
<Button onclick={(event) => { if (locked) event.preventDefault(); }}>Archive</Button>One naming rule for callbacks
Lowercase on<event> props are DOM handlers on the root element. camelCase on<Name> props are typed component callbacks (onValueChange, onOpenChange, onDismissRequest); a callback that can be vetoed gets an event with preventDefault().
<Select onValueChange={(value) => save(value)} onkeydown={trackKeys} />The Field rule
The id, aria-describedby, aria-invalid and disabled that a Field hands its control reach the focusable element, so the label focuses it and its description and error are announced: the input of TagsInput, the first thumb of a Slider, the first segment of a DatePicker.
<Field label="Tags" description="Up to five"> {#snippet children(control)}<TagsInput {...control.props} />{/snippet} </Field>Providers render nothing
Roots that only hold state (menu, select, popover, dialog and toast providers, MotionProvider) and portals render no element of their own, so they take no ref or rest attributes.
<MotionProvider level="subtle"><App /></MotionProvider>One motion prop
Components that move take motion: { enter, exit, layout, duration } or false. The names are the closed spec vocabulary; never CSS, keyframes or milliseconds, and unknown names fall back to the default.
<Dialog title="Edit crew" motion={{ enter: 'rise', exit: 'fade', duration: 'slow' }} />
Package docs
The site does not render the package docs yet. Read them in the repository:
- Core motion texels:
packages/core/docs/core-motion.md - Motion tokens:
packages/tokens/docs/tokens-motion.md - The motion prop and MotionProvider:
packages/texuli/docs/ui-motion.md - The spec motion vocabulary:
packages/spec/docs/motion.md - Node motion in the player:
packages/player/docs/reference.md - The component API contract:
docs/STANDARDS.md - ADR-013: the motion runtime:
docs/DECISIONS.md

