Ballmac UI home

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.

Motion settings, their variables, ranges and defaults
SettingVariableRangeDefault
SpeedPlayback speed. 2 halves every duration, 0.5 doubles them.--bm-speed0.5 to 2.51
BounceHow far springs overshoot before they settle. 0 never overshoots.--bm-bounce0 to 10.25
TravelHow far things move when they enter. 0 turns movement into a plain fade.--bm-travel0 to 21
HoverLift on hover and depth on press.--bm-hover0 to 21
StaggerThe gap between items that enter one after another.--bm-stagger0 to 21
Decorative loopsGlows, shimmers and drifting backgrounds. Off holds them still.--bm-ambient1 or 0on

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-calm

The 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-75 to duration-1000 and delay-75 to delay-1000 are divided by the speed.
  • ease-out, ease-in and ease-in-out use Ballmac's curves, and ease-spring is a new class: a real spring, written as a CSS linear() easing from the bounce setting. A browser that cannot parse linear() 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

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

components/toast.tsx
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.

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.