Skip to content
blaze-motion

blaze-motion

Motion presets

Spread onto your own elements. No wrappers, no extra DOM nodes.

Copy-paste through a shadcn registry. The files land in your repo and you import them from your own paths.

Setup

Requires React 19 — the provider uses two React 19-only constructs (context-as-provider, ref as a plain prop) and breaks on React 18.

  1. Add the engine

    npx shadcn@latest add https://motion.asmitsah.dev/r/motion.json
  2. Mount the provider once, at the root

    import type { ReactNode } from "react";
    import { MotionProvider } from "@/lib/motion";
    
    export default function RootLayout({ children }: { children: ReactNode }) {
      return (
        <html lang="en">
          <body>
            <MotionProvider>{children}</MotionProvider>
          </body>
        </html>
      );
    }

    A lazily-loaded feature bundle behind LazyMotion strict, with reducedMotion="user" set. Under that preference motion cuts transform and positional properties — x, y, scale — straight to their end value and animates the rest. Each preset table names which is which.

Usage

import { Section } from "@/lib/motion";
import { fadeUp } from "@/lib/motion/presets/fade-up";

export function Pricing() {
  return (
    <Section {...fadeUp} className="grid gap-6 md:grid-cols-3">
      <PlanCard plan="Starter" />
      <PlanCard plan="Team" />
      <PlanCard plan="Scale" />
    </Section>
  );
}

A preset is a plain object and the barrel carries no "use client", so a Server Component can import one as data.

Customisation

Globally — config.ts

Every tunable number lives here. No literal value appears anywhere else; every preset derives from this file.

// lib/motion/config.ts — yours. An update never overwrites it.
import type { ViewportOptions } from "motion/react";

type CubicBezier = [number, number, number, number];

export const duration = {
  fast: 0.35,
  base: 0.55,
} as const;

export const travel = { sm: 16, md: 28 } as const;

export const staggerStep = 0.08;

export const ease: CubicBezier = [0.16, 1, 0.3, 1];

export const fade = { from: 0, to: 1 } as const;

export const zoom = { in: 0.94, out: 1.06 } as const;

export const blur = { from: 8, to: 0 } as const;

export const rest = { x: 0, y: 0, scale: 1 } as const;

// `amount` must be "some", never a number — a numeric threshold leaves a taller-than-viewport
// element stuck at its initial state forever.
export const viewport: ViewportOptions = {
  once: true,
  amount: "some",
  margin: "0px 0px -12% 0px",
};

Once — after the spread

import { Section } from "@/lib/motion";
import { fadeUp } from "@/lib/motion/presets/fade-up";

export function Hero() {
  return (
    <Section {...fadeUp} transition={{ duration: 1.2, ease: "easeOut" }}>
      <h2>Slower, just here</h2>
    </Section>
  );
}

In sequence — withDelay

Returns a copy of the preset with a delay on its transition. It sequences siblings that are not a list — a stagger needs one parent owning the variants and children that inherit them.

import { H1, P, staggerStep, withDelay } from "@/lib/motion";
import { slideUp } from "@/lib/motion/presets/slide-up";

export function Hero() {
  return (
    <section>
      <P {...slideUp}>blaze-motion</P>
      <H1 {...withDelay(slideUp, staggerStep)}>
        Your markup does not change shape.
      </H1>
      <P {...withDelay(slideUp, staggerStep * 2)}>
        Spread onto the elements you already wrote.
      </P>
    </section>
  );
}

Your own presets

A preset is a plain object typed TargetPreset — the annotation every shipped preset carries, and what withDelay takes. VariantPreset types the other shape, a named hidden and visible pair, which is what staggerParent and staggerChild are.

import {
  type TargetPreset,
  duration,
  ease,
  rest,
  travel,
  viewport,
} from "@/lib/motion";

export const tiltIn: TargetPreset = {
  initial: { x: -travel.md, rotate: -3 },
  whileInView: { x: rest.x, rotate: 0 },
  viewport,
  transition: { duration: duration.base, ease },
};

Presets

fadeUpThe headline entrance. Content fades in from transparent while travelling up into its resting position as it enters the viewport.fadeDownStarts above its resting position and fades in on the way down, which reads well for panels that belong to something overhead.fadeLeftEnters from the right and fades leftward into rest, suiting content that should read as arriving from off the right edge.fadeRightThe mirror of fadeLeft on the x axis: it starts to the left of rest and fades rightward into place.slideUpTravels up into its resting position over a short offset, animating the y transform and nothing else.slideDownStarts above its resting position and settles down into place, animating the y transform and nothing else.slideLeftEnters from the right and travels leftward into rest, animating the x transform and nothing else.slideRightEnters from the left and travels rightward into rest, animating the x transform and nothing else.zoomInScales up from slightly smaller than rest until it settles at full size, animating scale and nothing else.zoomOutEases down from slightly larger than rest to full size, reading as a recede rather than a growth.staggerA pair that lands a list one item at a time: staggerParent times the sequence, staggerChild plays each item. The parent animates nothing of its own — it names the hidden and visible variants its children inherit and the step between them. The child carries the opacity and y, and needs a parent above it.blurInResolves a blur filter to zero while fading in from transparent, so the content settles into focus instead of travelling from a direction.