Docs
Motion
A theme is colour, shape and now movement. Six settings describe how a product moves; every duration, easing, lift and stagger is derived from them.
Try it on real components in the theme builder, or install a feel and tune it by hand.
The six settings
Each is a CSS variable on :root. The defaults reproduce Ballmac UI exactly as it was, so adding the theme changes nothing until you change a setting.
| Setting | Variable | Range | Default |
|---|---|---|---|
| SpeedPlayback speed. 2 halves every duration, 0.5 doubles them. | --bm-speed | 0.5 to 2.5 | 1 |
| BounceHow far springs overshoot before they settle. 0 never overshoots. | --bm-bounce | 0 to 1 | 0.25 |
| TravelHow far things move when they enter. 0 turns movement into a plain fade. | --bm-travel | 0 to 2 | 1 |
| HoverLift on hover and depth on press. | --bm-hover | 0 to 2 | 1 |
| StaggerThe gap between items that enter one after another. | --bm-stagger | 0 to 2 | 1 |
| Decorative loopsGlows, shimmers and drifting backgrounds. Off holds them still. | --bm-ambient | 1 or 0 | on |
Five ready-made feels
Each is a registry theme that changes movement only and leaves your colours alone. Installing another replaces it.
- Native: The Ballmac default: short fades, a soft settle on spatial moves and a light lift on hover. Reads as the platform, not as an effect.
@ballmac/motion-native - Calm: Longer, softer transitions with no bounce and short travel. Suits products people use for a long time, like editors, health and finance.
@ballmac/motion-calm - Snappy: Short durations, tight stagger and very little bounce. Suits dense tools and dashboards where speed is the feature.
@ballmac/motion-snappy - Playful: Clear overshoot, longer travel and a pronounced hover lift. Suits consumer apps, creative tools and marketing pages.
@ballmac/motion-playful - Still: Ships the reduced-motion values to every visitor: no travel, no lift, no loops, no bounce. Opacity changes stay so state remains visible.
@ballmac/motion-still
npx shadcn@latest add @ballmac/motion-calmThe twelve colour themes each carry a feel that suits them (Graphite and Mono are snappy, Sand, Forest and Midnight are calm, Violet and Ember are playful), so installing @ballmac/theme-violet brings its motion too.
What follows automatically
The theme rewrites Tailwind's own scales, so code you already have follows without edits:
duration-75toduration-1000anddelay-75todelay-1000are divided by the speed.ease-out,ease-inandease-in-outuse Ballmac's curves, andease-springis a new class: a real spring, written as a CSSlinear()easing from the bounce setting. A browser that cannot parselinear()ignores it and keeps the previous easing.- Dialogs, popovers, menus, tooltips and accordions: the enter and exit animations from
tw-animate-css(animate-in,animate-out, the accordion and collapsible animations) take their default duration from the speed. - Every Ballmac component: transitions, hover lift, button press, and decorative loops.
- The shared motion presets (
spring,duration,ease,variants,stagger) that Motion-based components use.
In your own CSS
.card {
transition: transform var(--bm-duration-base) var(--bm-ease-out);
}
.card:hover {
transform: translateY(var(--bm-lift)); /* -2px at hover 1, 0 at hover 0 */
}
.card:active {
transform: scale(var(--bm-press)); /* 0.97 at hover 1 */
}
.toast {
transform: translateY(var(--bm-distance)); /* enter distance, 10px at travel 1 */
transition: transform var(--bm-duration-slow) var(--bm-ease-spring);
}
.list > * {
transition-delay: calc(var(--i) * var(--bm-stagger-step));
}
.glow {
animation: pulse 2s infinite;
animation-play-state: var(--bm-ambient-state); /* running or paused */
}In JavaScript
@ballmac/motion-presets reads the same variables. Import the constants for one-off values, or call useMotion() when a component should follow the theme live.
import { motion } from "motion/react"
import { useLoopsPaused, useMotion } from "@/lib/ballmac/motion"
export function Toast({ children }: { children: React.ReactNode }) {
const m = useMotion()
return (
<motion.div
initial={{ opacity: 0, y: m.distance }}
animate={{ opacity: 1, y: 0 }}
transition={m.spring.snappy}
>
{children}
</motion.div>
)
}
// Decorative loops hold still when the theme turns them off, or under reduced motion.
function Glow() {
const still = useLoopsPaused()
return <motion.span animate={still ? undefined : { opacity: [0.4, 1, 0.4] }} transition={{ duration: 2, repeat: Infinity }} />
}Spinners and "thinking" indicators carry meaning, so they should keep running; only decorative loops use useLoopsPaused.
Reduced motion
The theme includes a prefers-reduced-motion rule: no travel, no lift, no bounce, no loops, and everything a little faster. Fades remain, because a change of state must stay visible. Reduced motion is not no motion.
Still ships those reduced values to every visitor, for products that should hold perfectly still. In the builder, Preview reduced motion shows exactly what a visitor with the setting sees.
Links and install URLs
A feel has a short address, and any combination is a link. Nothing is stored.
# a ready-made feel
https://ui.ballmac.com/themes?mo=playful
# or any values: speed, bounce, travel, hover, stagger, ambient (1 or 0), reduce (system or always)
https://ui.ballmac.com/themes?sp=1.3&bn=0.6&tv=0.8&hv=1.2&st=0.5&am=0
# install a custom design, colour and motion together
npx shadcn@latest add "https://ui.ballmac.com/themes/custom.json?h=262&mo=calm"Every value is clamped to its range, so a link can never produce an unsafe theme.