Docs · 11 pages
Docs / Style
Motion tokens and presets
Five sampled springs, one attribute to switch the whole page, and the durations, travel and stagger every component resolves from, as CSS custom properties and an opt-in Tailwind v4 layer.
No component hard-codes a curve, a duration, a travel distance or a stagger.
Everything resolves from six tokens on :root, and those six are filled by
the active preset:
| Preset | Spring (stiffness / damping) | Settles in | Feel |
|---|---|---|---|
snap |
1218 / 70 | 290 ms | clicks into place |
ui (default) |
305 / 33 | 450 ms | product UI |
gentle |
110 / 20 | 760 ms | editorial |
lively |
622 / 17 | 840 ms | overshoots, bounces back |
ambient |
43 / 13 | 1390 ms | slow, atmospheric |
Each spring is a real mass–spring–damper simulation sampled into a CSS
linear() function, with the time it takes to settle within 0.1% as the
duration. That is a spring without a physics runtime: the curve is a
timing function, the browser interpolates it, and it costs nothing per frame.
Switch the page
<html data-ma-preset="lively">Any subtree can carry its own: <section data-ma-preset="snap">. The
attribute sets these six tokens; components read them:
--ma-ease /* the sampled spring */
--ma-duration /* its settle time */
--ma-stagger /* 40 / 60 / 120 ms */
--ma-travel-enter /* 16–48 px, how far an entrance travels */
--ma-travel-hover /* 2–6 px */
--ma-travel-section /* 48 px, page-level motion */The homepage’s preset switcher is five radio inputs and a :root:has() rule
per preset: the same tokens, switched with zero JavaScript.
Fixed curves
Exits and continuous motion never use a spring:
| Token | Value | Use |
|---|---|---|
--ma-ease-out |
cubic-bezier(0.23, 1, 0.32, 1) |
exits, hover release |
--ma-ease-in |
cubic-bezier(0.5, 0, 0.75, 0) |
things leaving |
--ma-ease-in-out |
cubic-bezier(0.77, 0, 0.175, 1) |
ambient loops |
--ma-ease-drawer |
cubic-bezier(0.32, 0.72, 0, 1) |
sheets |
--ma-duration-fast |
150ms |
exits, hover, press |
--ma-duration-slow |
700ms |
long reveals |
Override any of them in your own CSS: :root { --ma-ease: … }. The base
stylesheet has zero specificity, so yours wins. A prop on a component
(<Reveal duration={900}>) sets a component-local property that wins over
the preset for that instance only.
Tailwind v4
@import 'tailwindcss';
@import '@moonarc/core/styles';That adds ease-ma / duration-ma (the active preset), ease-snap …
ease-ambient and duration-snap … duration-ambient (the presets by name),
plus ease-out-quint, ease-in-quint, ease-in-out-quint, ease-drawer,
duration-fast, duration-base, duration-slow as real utilities mapped onto
the same custom properties.
Tailwind is never required. Components ship their own layered CSS; this file is the only Tailwind-specific thing in the package, and it is opt-in.
Cascade layers
The base stylesheet declares @layer theme, base, components, utilities and
puts component rules in components. Tailwind v4 puts utilities in
utilities, so class="p-0" on a component always wins. Without Tailwind,
any unlayered rule of yours beats the library. Either way you never fight
specificity.
The flip side: a reset of your own that is unlayered beats the library too. A
hand-written a { color: inherit } outside any layer recolours every link a
component draws, including a primary button’s label, which then sits on the
primary colour in your body text colour. Put resets in @layer base, where
Tailwind’s preflight already is.