Guidelines

Motion

Builtwell moves with intent: quick to start, slow to settle. Motion confirms what someone did or shows where something came from. It never decorates for its own sake.

Curves

Custom properties on :root in packages/design-system/styles/system/easings.css. Use them in CSS as ease-(--ease-default) or transition-timing-function: var(--ease-default).
--ease-out: cubic-bezier(0.28, 0.44, 0.49, 1)
--ease-outcubic-bezier(0.28, 0.44, 0.49, 1)

A gentler arrival that starts moving at once. Hovers and small UI.

--bounce: cubic-bezier(0.6, 0, 0.1, 1.4)
--bouncecubic-bezier(0.6, 0, 0.1, 1.4)

Overshoots and settles. For a playful moment, once per page at most.

--ease-out-soft: cubic-bezier(0.28, 0, 0.49, 1)
--ease-out-softcubic-bezier(0.28, 0, 0.49, 1)

Like ease-out with a softer start. Fades and colour.

--ease-in-out-soft: cubic-bezier(0.72, 0, 0.28, 1)
--ease-in-out-softcubic-bezier(0.72, 0, 0.28, 1)

Things that travel from A to B and stop: carousels, sliders.

--ease-in-out-hard: cubic-bezier(0.77, 0, 0.175, 1)
--ease-in-out-hardcubic-bezier(0.77, 0, 0.175, 1)

Big, deliberate moves: page transitions, a full-screen menu.

--ease-default: cubic-bezier(0.5, 0, 0, 1)
--ease-defaultcubic-bezier(0.5, 0, 0, 1)

The house curve, and --default-ease. A quick start and a long, soft landing: reveals, panels, anything that arrives.

--ease-default
--ease-out
--ease-in-out-hard
--bounce
linear

In use

Hover the button, replay the entrance. Both run the real code.
Start a project
.bw-button: the fill slides in, 300ms
  • Branding
  • Websites
  • Marketing
FadeUp on --ease-default, 600ms, 80ms stagger

Durations

MomentDurationWhere
Hover fill on .bw-button300msstyles/globals.css
Accordion and collapsible200msstyles/globals.css, @theme inline
FadeUp entrance, 20px of travel600mscomponents/animations/fade-up.tsx
Stagger between items in a row60 to 120msIndex cards, prototypes
Page transition, a cross-fade1300msapp/(website)/template.tsx

Interface feedback stays under 300ms. Content that enters runs 500 to 700ms. Only whole-page moves take longer than a second.

Tools

Two libraries, each with a job. motion/react (Motion, not framer-motion) for component state and entrances. GSAP with ScrollTrigger, set up in apps/web/components/gsap, for scroll-linked timelines; it runs on the Tempus clock and reads Lenis scroll, so both stay in sync.
"use client";
import { motion, useReducedMotion } from "motion/react";

export function Reveal({ children }: { children: React.ReactNode }) {
  const reduceMotion = useReducedMotion();
  return (
    <motion.div
      initial={{ opacity: 0, y: reduceMotion ? 0 : 20 }}
      transition={{ duration: 0.6, ease: [0.5, 0, 0, 1] }} // --ease-default
      viewport={{ once: true, amount: 0.3 }}
      whileInView={{ opacity: 1, y: 0 }}
    >
      {children}
    </motion.div>
  );
}
import { easings } from "@repo/design-system/easings";

gsap.to(element, {
  yPercent: -20,
  ease: "none", // scrubbed timelines stay linear
  scrollTrigger: { trigger: element, scrub: true },
});

// For a timed tween, pass a named curve
element.animate(frames, { duration: 600, easing: easings["out-quint"] });

easings.ts

The named curves exported from packages/design-system/easings.ts, the classic Penner set plus gleasing, for JavaScript animation.
in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53)in-quad
in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19)in-cubic
in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22)in-quart
in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06)in-quint
in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035)in-expo
in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335)in-circ
out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94)out-quad
out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1)out-cubic
out-quart: cubic-bezier(0.165, 0.84, 0.44, 1)out-quart
out-quint: cubic-bezier(0.23, 1, 0.32, 1)out-quint
out-expo: cubic-bezier(0.19, 1, 0.22, 1)out-expo
out-circ: cubic-bezier(0.075, 0.82, 0.165, 1)out-circ
in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955)in-out-quad
in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1)in-out-cubic
in-out-quart: cubic-bezier(0.77, 0, 0.175, 1)in-out-quart
in-out-quint: cubic-bezier(0.86, 0, 0.07, 1)in-out-quint
in-out-expo: cubic-bezier(1, 0, 0, 1)in-out-expo
in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86)in-out-circ
gleasing: cubic-bezier(0.4, 0, 0, 1)gleasing

Keyframes in styles/system/animations.css: pulse, fadeIn, accordion-down, accordion-up, collapsible-down, collapsible-up, grow-left-to-right, grow-right-to-left, grow-top-to-bottom, grow-bottom-to-top, slide-up.

Rules

  • Animate transform and opacity. Never width, height, top or left on anything that reflows the page.
  • Respect reduced motion: with useReducedMotion, travel and scale go, fades stay.
  • Import from motion/react. framer-motion is not a dependency.
  • Scroll-linked motion is scrubbed and linear (ease: none). Easing belongs to timed tweens.
  • Exits are quicker than entrances; hovers return without delay.
  • --bounce is the exception, not the default. Nothing loops or moves on its own except a ticker.