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.

  1. animateWeb Animations, one run
  2. presenceenter and exit, reversible
  3. flipreorder, insert, remove
  4. sharedone element, two places
  5. timelineplay, pause, seek, reverse
  6. staggeroffsets, capped at 1 s
  7. springa pure solver, linear() curves
  8. levelsfull, 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.

Motion level

Your system: checking

  • full Every preset plays at its token duration.
  • subtle Durations shrink to 60%, and travel and scale are dropped: a move commits at once, a fade stays.
  • none Nothing animates: every change commits at once.

Not played yet

Duration tokens at this level
fastbaseslowslower
150 ms200 ms300 ms500 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.

  • fade

    Keyframes load with the runtime.

  • rise

    Keyframes load with the runtime.

  • scale

    Keyframes load with the runtime.

  • slide-start

    Keyframes load with the runtime.

  • slide-end

    Keyframes load with the runtime.

  • edge slide, left

    Keyframes load with the runtime.

  • edge slide, right

    Keyframes load with the runtime.

  • edge slide, top

    Keyframes load with the runtime.

  • edge slide, bottom

    Keyframes 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.

Presence options
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.

32 package sources that animate through the runtime
ComponentPackageWhat movesSee it
DialogtexuliPanel scale in and out over a fading backdrop, baseOn this page
AlertDialogtexuliPanel scale in and out over a fading backdrop, baseOn this page
SheettexuliSlides from its edge the whole way (rise), baseOn this page
Sheet overlaytexuliBackdrop fade in and outOn this page
DrawertexuliSlides from its edge to its snap point, baseOn this page
DetailPaneltexuliSlides from its edge, baseOn this page
ResponsiveModaltexuliDialog or bottom drawer, slides from its edge, baseOn this page
PopovertexuliScale in and out, fastOn this page
HoverCardtexuliScale in, fade out, fastOn this page
TooltiptexuliScale in, fade out, fastOn this page
DropdownMenutexuliScale in, fade out, fastOn this page
SubmenutexuliScale in, fade out, fastOn this page
ContextMenutexuliScale in, fade out, fastOn this page
MenubartexuliScale in, fade out, fastOn this page
ToastertexuliRise in, fade out; the stack glides (layout), baseOn this page
TabstexuliThe indicator glides to the new tab (layout), baseOn this page
AccordiontexuliPanel height opens and closes (layout), baseOn this page
Dashboard@texuli/dataWidgets glide to their new cell after a change (FLIP), in development/dashboards-page/
DataGrid@texuli/dataRows glide on sort, insert and remove (FLIP), baseIts showcase page is planned
Board@texuli/dataCards glide on a committed move (FLIP), baseOn this page
InteractivePlot (every interactive chart)@texuli/chartsFollows the MotionProvider reduced-motion setting and the OS preference (data-reduced-motion)On this page
Board, DataGrid and Dashboard list motion@texuli/dataLoads texuli/motion list motion only once a change can animateOn this page
SchemaForm@texuli/formsEach new step fades in, fast/forms-page/#flow
PageEditor blocks@texuli/pagesBlocks glide on insert, remove and move (FLIP), fast/solutions/documents/
PageEditor@texuli/pagesTakes the motion prop and passes it to its block list/solutions/documents/
Editor context@texuli/pagesShares the editor’s motion prop with its rows/solutions/documents/
CollabEditor@texuli/pagesThe same block motion in collaborative editing/solutions/documents/
Canvas zoom and fit@texuli/canvasZoom and fit glide through the runtime/solutions/canvas/
Player motion settings@texuli/playerThe level a blueprint or host setsOn this page
Blueprint node motion@texuli/playerEnter, exit, layout and stagger declared as dataOn this page
Slide transitions and fragments@texuli/slidesEvery transition and fragment effect at the deck’s levelOn this page
Stage motion (static half)@texuli/slidesReads fragments before a step change and loads the runtime halfOn 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

  1. Write launch notes
  2. Refresh pricing page
  3. Check status page
  4. 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 cards
Audit the forms
Dark mode pass
CSV export

Doing

1 card
Search index

Done

1 card
Onboarding emails

Shared elements: one card, two places

Open a card and its preview grows into the detail view; go back and it returns to the grid. The registry measures the element before the change and plays the new one from that box.

  • Harbor Notes
  • Tide Board
  • Lumen Docs
  • Orbit Status

Level full, reduced motion follows your system Not played yet Change the settings

Timelines 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.

Position over time for the gentle, snappy and bouncy springs: each rises from 0 to 1; bouncy overshoots the most.0 ms200 ms400 ms600 ms800 ms10
Drawn from springSamples in @texuli/core/motion/spring, the solver that precomputed each token's linear() easing.
  • gentle stiffness 120, damping 20, mass 1; ratio 0.91; settles in 648 ms; peak 1
    --tx-motion-spring-gentle
  • snappy stiffness 400, damping 30, mass 1; ratio 0.75; settles in 453 ms; peak 1.03
    --tx-motion-spring-snappy
  • bouncy stiffness 380, damping 18, mass 1; ratio 0.46; settles in 844 ms; peak 1.19
    --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.

Position over 718 ms for stiffness 170, damping 26, mass 1: peak 1.10718 ms
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

Five duration steps (theme defaults)
StepDefaultCustom propertyPlay
instant0 ms--tx-motion-instant
fast150 ms--tx-motion-fast
base200 ms--tx-motion-base
slow300 ms--tx-motion-slow
slower500 ms--tx-motion-slower

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…

What each slide transition plays, from resolveSlideTransition
Transitionfullsubtlenonereduced motion
nonecutcutcutcut
fadefade, 300 msfade, 180 mscutfade, 150 ms
fade-upfade-up, 300 msfade-up, 180 mscutfade, 150 ms
slideslide, 400 msslide, 240 mscutfade, 150 ms
zoomzoom, 350 msfade, 180 mscutfade, 150 ms
morphmorph, 400 msmorph, 240 mscutfade, 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

Blueprint

Motion blueprint (untrusted)

Untrusted content Draft

No 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/motion

    The runtime texels: 39 exports

    • DEFAULT_MOTION_DURATION
    • DEFAULT_MOTION_EASING
    • DEFAULT_SPRING
    • DEFAULT_SPRING_SAMPLES
    • MAX_MOTION_MS
    • MAX_SPRING_MS
    • MAX_SPRING_SAMPLES
    • MAX_STAGGER_SPAN
    • MAX_TIMELINE_STEPS
    • MOTION_LEVELS
    • REDUCED_FADE_MS
    • SUBTLE_DURATION_SCALE
    • animate
    • createFlip
    • createMotionPresence
    • createReducedMotionSource
    • createRun
    • createSharedElements
    • dampingRatio
    • flipTransform
    • isMotionLevel
    • lowerMotionLevel
    • motionDuration
    • normalizeSpring
    • parallel
    • resolveMotion
    • resolveMotionLevel
    • retargetVelocity
    • sequence
    • springEasing
    • springKeyframes
    • springSamples
    • springSettle
    • springState
    • stagger
    • startAnimation
    • startScopedViewTransition
    • timeline
    • withoutTravel
  • @texuli/core/motion/spring

    The spring solver: 12 exports

    • DEFAULT_SPRING
    • DEFAULT_SPRING_SAMPLES
    • MAX_SPRING_MS
    • MAX_SPRING_SAMPLES
    • dampingRatio
    • normalizeSpring
    • retargetVelocity
    • springEasing
    • springKeyframes
    • springSamples
    • springSettle
    • springState
  • @texuli/tokens/motion

    The motion tokens: 13 exports

    • MOTION_DURATIONS
    • MOTION_DURATION_STEPS
    • MOTION_EASINGS
    • MOTION_EASING_NAMES
    • MOTION_SPRINGS
    • MOTION_SPRING_NAMES
    • MOTION_SPRING_SAMPLES
    • MOTION_VARIABLE_NAMES
    • motionDurationVar
    • motionEasingVar
    • motionSpringVars
    • motionVariables
    • slowerDuration
  • texuli/motion

    The component contract and seams: 18 exports

    • DEFAULT_MOTION_SETTINGS
    • MOTION_DURATION_NAMES
    • MOTION_LEVEL_NAMES
    • MOTION_PRESETS
    • MotionProvider
    • createIndicatorMotion
    • createListMotion
    • createOverlayMotion
    • createPresenceMotion
    • createSizeMotion
    • getMotionSettings
    • isMotionLevelName
    • isMotionPreset
    • loadMotionRuntime
    • lowerLevel
    • motionRuntime
    • resolveComponentMotion
    • setMotionSettings
16 custom properties with their theme defaults
Custom propertyDefault
--tx-motion-instant0ms
--tx-motion-fast150ms
--tx-motion-base200ms
--tx-motion-slow300ms
--tx-motion-slower500ms
--tx-motion-ease-standardcubic-bezier(0.4, 0, 0.2, 1)
--tx-motion-ease-emphasizedcubic-bezier(0.2, 0, 0, 1)
--tx-motion-ease-entercubic-bezier(0, 0, 0.2, 1)
--tx-motion-ease-exitcubic-bezier(0.4, 0, 1, 1)
--tx-motion-ease-linearlinear
--tx-motion-spring-gentlelinear(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-duration648ms
--tx-motion-spring-snappylinear(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-duration453ms
--tx-motion-spring-bouncylinear(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-duration844ms

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 motion

One 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