Skip to content
Logic2BUI

Search docs

Search components and documentation

New
Menu

Motion

Enter, exit and hover animation recipes on tokens — built on tw-animate-css with zero runtime dependencies, plus an optional Framer Motion flavor.

fade
fade-up
fade-left
scale
blur

Installation

npx logic2b@next add motion

The motion engine ships the <Motion> primitive together with the recipe maps and helpers. Each named preset is also installable on its own — motion-fade, motion-slide, motion-scale and motion-blur — and each one pulls in motion automatically.

Usage

import { Motion } from "@/components/ui/motion"

<Motion preset="fade-up">
  <Card>…</Card>
</Motion>

<Motion> plays its enter recipe once, on mount, and then gets out of the way. It renders a div by default; pass asChild to animate the child element directly instead of adding a wrapper node:

<Motion preset="scale" asChild>
  <Card>…</Card>
</Motion>

Presets

Preset Effect
fade Opacity 0 → 1
fade-up Fade while rising from below
fade-down Fade while dropping from above
fade-left Fade while entering from the right
fade-right Fade while entering from the left
scale Fade while zooming from 95%
blur Fade while sharpening from an 8px blur

Timing

Duration and delay are props — no arbitrary Tailwind class is generated at runtime, so any value works. delay is the knob for staggering a list:

{items.map((item, i) => (
  <Motion key={item.id} preset="fade-up" duration={500} delay={i * 80}>
    <Item {...item} />
  </Motion>
))}

The default is a 500ms ease-out enter. Override the easing with a className (ease-in-out, ease-linear, …) — it merges over the default.

Hover

Pass a hover recipe to add an interaction on top of the enter animation:

<Motion preset="fade" hover="lift">
  <Card>…</Card>
</Motion>
Hover Effect
lift Rises 4px
sink Presses down 2px
scale Grows to 103%
glow Raises the shadow

Exit animations

<Motion> covers the mount case. For an element that also animates out — a dialog, a popover, anything with a Radix data-[state] — reach for the recipe maps and drive both states off data-state instead:

import {
  motionEnterPresets,
  motionExitPresets,
} from "@/components/ui/motion"

// Applied to a Radix content node:
<DialogContent
  className={cn(
    "data-[state=open]:animate-in data-[state=closed]:animate-out",
    "data-[state=open]:fade-in data-[state=open]:zoom-in-95",
    "data-[state=closed]:fade-out data-[state=closed]:zoom-out-95",
  )}
/>

motionEnterPresets / motionExitPresets hold the exact class strings for every preset if you’d rather look them up than hand-write the pair. The motionEnter, motionExit and motionHover helpers build the full class string (animation utility + timing + reduced-motion guard) for you.

Reduced motion

Every recipe degrades under prefers-reduced-motion: the enter animation is dropped so content appears immediately (motion-reduce:animate-none) and hover transitions are disabled (motion-reduce:transition-none). No extra wiring needed.

The Framer Motion flavor

The presets above are deliberately CSS-only: they cost nothing at runtime and handle the enter/hover cases most interfaces need. When you want spring physics, shared-layout transitions or exit animations on plain (non-Radix) elements, drop in the optional Framer Motion flavor of the same recipe. Install framer-motion, then copy this alongside the CSS engine:

"use client"

import * as React from "react"
import { AnimatePresence, motion, type Transition } from "framer-motion"

const spring: Transition = { type: "spring", stiffness: 300, damping: 30 }

const variants = {
  fade: { hidden: { opacity: 0 }, visible: { opacity: 1 } },
  "fade-up": {
    hidden: { opacity: 0, y: 12 },
    visible: { opacity: 1, y: 0 },
  },
  scale: {
    hidden: { opacity: 0, scale: 0.95 },
    visible: { opacity: 1, scale: 1 },
  },
} as const

export function MotionSpring({
  preset = "fade-up",
  show = true,
  children,
}: {
  preset?: keyof typeof variants
  show?: boolean
  children: React.ReactNode
}) {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          initial="hidden"
          animate="visible"
          exit="hidden"
          variants={variants[preset]}
          transition={spring}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  )
}

Same preset names, same tokens — spring feel and real exit animations when you need them, and nothing added to your bundle when you don’t.

Live playground

Edit the component props and inspect the exact JSX before copying it. The preview loads only when this section enters the viewport.

Generated JSX
<Motion
  preset="fade-up"
  hover="lift"
  duration={500}
  delay={0}
>Animated content</Motion>

API reference

Generated from the public TypeScript exports in src/ui/motion.tsx.

View JSON

MotionPreset

Type

Motion presets — enter/exit/hover recipes expressed as token-driven class strings on top of `tw-animate-css` (zero runtime deps). The `<Motion>` primitive plays an enter recipe once on mount; the exported maps and helpers let you drop the same recipes onto anything else (a Radix `data-[state]` element, a hand-rolled transition). Timing rides the CSS custom properties tw-animate-css reads: - default duration/easing come from the static `duration-*`/`ease-*` classes baked into `motionEnter`, - the `duration`/`delay` props override them per-instance via `--tw-animation-duration` / `--tw-animation-delay` (no arbitrary Tailwind class is generated at runtime, so it works with any value). Everything degrades under `prefers-reduced-motion`: the enter animation is dropped (content shows immediately) and hover transitions are disabled.

type MotionPreset = | "fade" | "fade-up" | "fade-down" | "fade-left" | "fade-right" | "scale" | "blur"

MotionHover

Type
type MotionHover = "lift" | "sink" | "scale" | "glow"

motionEnterPresets

Utility

Enter recipes: tw-animate-css `animate-in` sets the keyframe `from` state, the element animates to its natural rendered state.

motionExitPresets

Utility

Exit recipes: the mirror of each enter preset, for `animate-out` on a Radix `data-[state=closed]` element.

motionHoverPresets

Utility

Hover recipes: plain Tailwind transitions, no keyframes.

motionEnter

Utility

Build the class string for an enter recipe (plays on mount).

(preset: MotionPreset, className?: string): string

motionExit

Utility

Build the class string for an exit recipe (pair with `animate-out`).

(preset: MotionPreset, className?: string): string

motionHover

Utility

Build the class string for a hover recipe.

(hover: MotionHover, className?: string): string

MotionProps

Type
interface MotionProps extends React.ComponentProps<"div"> { /** Enter recipe to play on mount. Default `"fade"`. */ preset?: MotionPreset /** Optional hover recipe applied to the same element. */ hover?: MotionHover /** Enter duration in milliseconds. Overrides the 500ms default. */ duration?: number /** Enter delay in milliseconds — the knob for staggering a list. */ delay?: number /** Merge props onto the single child instead of rendering a wrapper. */ asChild?: boolean }

Motion

Component
Props: MotionProps
PropTypeRequiredDefaultDescription
presetMotionPresetNo"fade"Enter recipe to play on mount. Default `"fade"`.
hoverMotionHoverNo—Optional hover recipe applied to the same element.
durationnumberNo—Enter duration in milliseconds. Overrides the 500ms default.
delaynumberNo—Enter delay in milliseconds — the knob for staggering a list.
asChildbooleanNofalseMerge props onto the single child instead of rendering a wrapper.

Accessibility contract

Machine-readable behavior and consumer responsibilities for motion.

View JSON
Support
Composition contract
Pattern
animated semantic wrapper
Motion for React

Built-in semantics

  • Preserves the semantics and focus behavior of the rendered child element.

Consumer responsibilities

  • Respect prefers-reduced-motion and avoid animating focus, reading order or critical status changes.

Known limitations

  • • Motion safety depends on the selected transition and consumer composition.