agentleFS
shipiitshipiit/shipit-ui-design/skills/motion-design/SKILL.md

motion-design

Skill0 starsChanged 6 months agoMITView on GitHub

Pick easings, durations, stagger, and page-transition recipes for UI motion; auto-activates on `/motion` and motion-related edits across React/Vue/Svelte stacks.

46 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use addskill, apply-firmware and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • ryanportfolioSkill

    addskill

    Create, import, update, or install repo or personal skills: runtime ownership, resources, discovery checks.

    Also in 6 projects that use this

  • ryanportfolioSkill

    apply-firmware

    Bring current folder (plain folder, existing repo, old or partial firmware copy) up to full Harness Firmware in place. Mirror external repo → adopt-repo.

    Also in 6 projects that use this

  • +10 more from ryanportfolio

The file

We split this skill into the rules it gives an agent

5 rules. 0 are common, 0 are distinctive, and 5 are very specific to this project.

  • 0

    Common

    Most of the 42 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never introduce a motion library if one is already present in package.json.

    line 34
  2. Very specific:

    All durations are tokens. Never write a raw ms literal.

    line 40
  3. Very specific:

    Never use bare ease, ease-in, ease-out defaults — they are ugly.

    line 64
  4. Very specific:

    Use Framer Motion layoutId (React) or View Transitions API where supported.

    line 113
  5. Very specific:

    Every interactive element gets all four.

    line 140

“Common” is measured: kept by at least 6 in 10 of the 42 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Motion Design

Tasteful motion = imperceptible-when-needed, expressive-when-warranted. Defaults below come from tokens.css written by /design init.

Constitution (verbatim — applies to every artifact)

  1. Max 300 lines per file. If a generated component would exceed, split before writing.
  2. No hardcoded design values. Colors, spacing, radii, shadows, durations, easings — all from tokens.
  3. Every interactive element has hover, active, focus-visible, and disabled states.
  4. All motion respects prefers-reduced-motion.
  5. Every image / illustration has alt text or aria-hidden if decorative.
  6. Dark mode is never an afterthought — emitted alongside light from the start.
  7. Stack-respect: never introduce a new framework or styling system; adapt to what's there.

Library defaults (per stack)

Spec-locked defaults. Alternates noted, do not switch without explicit user request.

StackDefaultAlternate
ReactFramer MotionMotion (Matt Perry fork) — listed in spec section 11 as a build-time decision
Vanilla / non-ReactMotion One—
Heavy timelines, scrollytellingGSAP—
Vue / Svelte / Nuxtnative transitions + tokens.cssMotion One when complex

Never introduce a motion library if one is already present in package.json. Stack-respect (rule 7).


Duration ladder

All durations are tokens. Never write a raw ms literal.

TokenValueUse
--dur-150150 msMicro: tooltip, color flash, focus ring
--dur-200200 msHover, exit transitions, small toggles
--dur-300300 msLayout shifts, drawer/popover, dialog
--dur-600600 msEntrance, hero reveal, page fade

Above 600 ms is reserved for orchestrated sequences (/scene, scroll-driven). Never put > 600 ms on a click response.


Easing curves

Tokens, not hand-typed cubic-béziers.

TokenCurveUse
--ease-out-quintcubic-bezier(0.22, 1, 0.36, 1)Entrance, hover-in, layout grow
--ease-in-out-cubiccubic-bezier(0.65, 0, 0.35, 1)Symmetrical transitions, drawers
--ease-springspring (stiffness 300, damping 30)Layout, drag-release, success bounce
linearlinearProgress bars, scrubbing only

Never use bare ease, ease-in, ease-out defaults — they are ugly. Never use ease-in alone on entrance (feels stuck).


Default recipes

From spec section 6.6.

EventDurationEasing
Hover200 msease-out-quint
Active / press100 msease-out-quint
Layout shift300 msspring
Entrance600 msease-out-quint, with stagger
Exit200 msease-in-cubic
Page transition300 ms in / 200 ms outease-in-out-cubic

Stagger patterns

Stagger reveals natural reading order. Children: 40–80 ms apart. Cap total at ~400 ms so the last item is not perceived as late.

const list = { visible: { transition: { staggerChildren: 0.05 } } };
const item = { hidden: { opacity: 0, y: 8 },
  visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.22,1,0.36,1] } } };
<motion.ul variants={list} initial="hidden" animate="visible">
  {items.map(i => <motion.li key={i.id} variants={item}>…</motion.li>)}
</motion.ul>

Bad: 200 ms × 12 children (2.4 s — reads as broken); stagger on every list site-wide (wallpaper); stagger on every re-render (only mount or explicit transition).


Page transitions

Choose one and apply globally; never mix techniques on the same site.

Crossfade (default)

  • Out: 200 ms opacity 1 → 0
  • In: 300 ms opacity 0 → 1, y 8 → 0

Slide

  • Forward: new page from right, old to left, 300 ms ease-in-out-cubic
  • Back: reverse direction (use route-history hook)

Shared-element

  • Use Framer Motion layoutId (React) or View Transitions API where supported.
  • Reserve for hero → detail navigation. Never on every link.

prefers-reduced-motion

Mandatory. Opacity-only transitions remain allowed (no vestibular trigger). Translate, scale, rotate, parallax must be disabled.

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
import { useReducedMotion } from "framer-motion";
const reduced = useReducedMotion();
const variants = reduced
  ? { hidden: { opacity: 0 }, visible: { opacity: 1 } }
  : { hidden: { opacity: 0, y: 8 }, visible: { opacity: 1, y: 0 } };

Hover / active / focus baseline

Every interactive element gets all four. From tokens.css:

.btn {
  transition:
    transform var(--dur-200) var(--ease-out-quint),
    background-color var(--dur-200) var(--ease-out-quint),
    box-shadow var(--dur-200) var(--ease-out-quint);
}
.btn:hover  { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.btn:active { transform: translateY(0);    box-shadow: var(--shadow-sm); transition-duration: var(--dur-150); }
.btn:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }

Anti-patterns (do not generate)

  • transition: all <time> — force-specifies every property; explicit list only.
  • Infinite loops on UI chrome (spinners excepted; always have a stop condition).
  • Parallax on text bodies (vestibular trigger and reading hostile).
  • Bouncy springs on layout-blocking transitions (router, modal open).
  • Motion on first-paint above the fold without skeleton — perceived as jank.
  • Auto-playing carousels with no pause control.

Library cheat-sheet

  • Framer Motion — motion.<el>, variants, AnimatePresence, layout, useReducedMotion().
  • Motion One — animate(el, { y: [10, 0], opacity: [0, 1] }, { duration: 0.6, easing: [0.22, 1, 0.36, 1] }).
  • GSAP — timelines and ScrollTrigger only. Always guard with prefers-reduced-motion before gsap.to.
  • Vue/Svelte/CSS — <Transition> / transition: directives + token-driven classes. Never inline 0.3s literals.

Advanced motion patterns

Reach for these when a screen needs depth beyond hover/focus. Every pattern ships a reduced-motion fallback. Samples use motion/react — verify import path at build time.

Scroll-driven reveal

whileInView with { once: true, margin: "-15% 0px" } — fires once, slightly before entry. Stagger per word or per line; never per character. Reduced-motion: instant opacity, no y.

<motion.h1 initial="hidden" whileInView="visible" viewport={{ once: true, margin: "-15% 0px" }}
  transition={{ staggerChildren: 0.06 }}>
  {words.map((w, i) => (
    <motion.span key={i} variants={{ hidden: { opacity: 0, y: 12 }, visible: { opacity: 1, y: 0 } }}
      transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}>{w} </motion.span>
  ))}
</motion.h1>

Parallax

6–12 px translate, never above 24 px near text. Hero illustrations and decorative grids only. useScroll + useTransform. Disabled under reduced-motion.

Magnetic hover

Cursor-follow translate within ±8 px on a CTA. Gate on (pointer: fine) to skip touch.

const x = useMotionValue(0), y = useMotionValue(0);
const onMove = (e) => { if (!matchMedia("(pointer: fine)").matches) return;
  const r = e.currentTarget.getBoundingClientRect();
  x.set(((e.clientX - r.left) / r.width - 0.5) * 16);
  y.set(((e.clientY - r.top) / r.height - 0.5) * 16); };
<motion.button onMouseMove={onMove} onMouseLeave={() => { x.set(0); y.set(0); }} style={{ x, y }} />

Layout transitions

layout for inline reorder; layoutId for cross-route shared elements (thumb → detail hero). Pair with AnimatePresence. Never spring-bouncy on layout-blocking transitions like router or modal open.

<motion.li layout transition={{ type: "spring", stiffness: 300, damping: 30 }}>…</motion.li>
<motion.img layoutId={`cover-${id}`} src={src} />

Number counters

Animate 0 → target on viewport entry. Intl.NumberFormat for grouping. Reduced-motion: render final value directly.

const v = useMotionValue(0);
const display = useTransform(v, (n) => new Intl.NumberFormat().format(Math.round(n)));
useEffect(() => { animate(v, target, { duration: 1.2, ease: [0.22, 1, 0.36, 1] }); }, []);

Marquee / ticker

Logo grids and testimonial strips: duplicate list once, translate -50%, loop. Pause on hover. Reduced-motion: full-stop, single copy.

.marquee { display: flex; gap: var(--space-8); animation: scroll 30s linear infinite; }
.marquee:hover { animation-play-state: paused; }
@keyframes scroll { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .marquee { animation: none; } }

Page-transition reveal

Radial wipe or skew slide for route changes. Next.js App Router: place a template.tsx (re-renders on navigation), --dur-300 in / --dur-200 out, --ease-in-out-cubic.

export default function Template({ children }) {
  return <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.3, ease: [0.65, 0, 0.35, 1] }}>{children}</motion.div>; }

Animated borders

Two recipes: (a) conic-gradient rotation animating --angle; (b) SVG stroke stroke-dashoffset to zero. Reserve for the page's single most important CTA — never a list.

@property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.btn-glow { background: conic-gradient(from var(--angle), var(--color-brand-400), var(--color-accent-400), var(--color-brand-400));
  animation: spin 4s linear infinite; }
@keyframes spin { to { --angle: 360deg; } }

Mesh-gradient drift

Slow oscillation (15–30 s) of blurred radial blobs behind hero content. translate3d, not background-position (paint thrash). Reduced-motion: static.

<motion.div animate={{ x: [0, 40, -20, 0], y: [0, -30, 20, 0] }}
  transition={{ duration: 22, repeat: Infinity, ease: "easeInOut" }}
  style={{ background: "radial-gradient(closest-side, var(--color-brand-400), transparent)",
    filter: "blur(80px)", opacity: 0.25 }} />

Typewriter / word reveal

Word-by-word fade-up for display headlines, 60 ms gap. Reduced-motion: instant. Never per character.

Tilt cards

perspective(1000px) rotateX/Y ±8° tracking cursor. Coarse-pointer: no tilt. Pair with a same-axis shadow shift.

const onMove = (e) => { if (!matchMedia("(pointer: fine)").matches) return;
  const r = e.currentTarget.getBoundingClientRect();
  const px = (e.clientX - r.left) / r.width - 0.5, py = (e.clientY - r.top) / r.height - 0.5;
  e.currentTarget.style.transform = `perspective(1000px) rotateX(${-py*8}deg) rotateY(${px*8}deg)`; };

Card lift on hover

y: -2, --shadow-md, brand-tinted border. 200 ms ease-out. System default — do not invent per-component variants.

.card { transition: transform var(--dur-200) var(--ease-out-quint),
  box-shadow var(--dur-200) var(--ease-out-quint), border-color var(--dur-200) var(--ease-out-quint); }
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md);
  border-color: color-mix(in oklch, var(--color-brand-400) 40%, var(--color-border)); }

Cross-refs: rich-ui-patterns/SKILL.md (visual catalog), ui-design-principles/SKILL.md (motion-polish rubric).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.