motion-stacks
Writing motion in each stack: plain CSS and JavaScript, Tailwind, React and the Motion library, Next.js, Vue, Svelte, and the native mobile toolkits, each with reduced motion. Read before writing animation code in the project's stack.
- License 许可
- Apache-2.0
- 所属仓库
- enowdev/enowxcli
- 最近更新
- 2026-10-06
- Stars
- ★ 67
- Forks
- 21
01 前言 Front Matter
- name
- motion-stacks
- description
- Writing motion in each stack: plain CSS and JavaScript, Tailwind, React and the Motion library, Next.js, Vue, Svelte, and the native mobile toolkits, each with reduced motion. Read before writing animation code in the project's stack.
03 SKILL.md 正文
Motion in each stack
The rules are the same in every stack (motion); this is how each one
writes them. Whatever the stack: the tokens are defined once, reduced
motion is handled once at the root rather than in every component, only
opacity and transform move, and no library is added for what CSS already
does (motion-performance).
1. Plain CSS and JavaScript
- The tokens, in one place:
:root {
--m-ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
--m-ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19);
--m-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--m-instant: 100ms;
--m-fast: 160ms;
--m-base: 240ms;
--m-panel: 320ms;
--m-enter: 700ms;
--m-draw: 1000ms;
--m-shift-sm: 6px;
--m-shift: 12px;
--m-shift-lg: 18px;
}
- Keyframes named for what they do (
rise,fade,draw,count), with only afromwhen they end at the element's own style. - Movement written inside
@media (prefers-reduced-motion: no-preference)rather than undone insidereduce: a rule added later is then safe by default. - The reveal module from
motion-reveal, started once:startReveal(document.documentElement)at the end of<body>or onDOMContentLoaded. - Elements shown and hidden with
displayor[open](dialogs, popovers) animate in and out with@starting-styleandallow-discretetransitions (motion-interface). - One-off motion from script uses the Web Animations API, not a
requestAnimationFrameloop writing styles:el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 240, easing: "cubic-bezier(0.22, 0.61, 0.36, 1)" }). It runs like CSS and returns an animation you can await (.finished),cancel()orreverse(). - Replaying CSS animations (a drawing, on hover): give the parts
animation-fill-mode: both, then for each ofel.getAnimations({ subtree: true })setcurrentTime = 0and callplay(). A finished animation withbackwardsfill is no longer in effect, sogetAnimations()does not return it: replayable parts needboth. - Reduced motion in script:
matchMedia("(prefers-reduced-motion: reduce)"), and itschangeevent for anything that keeps running.
2. Tailwind CSS
Version 4 keeps the tokens in CSS:
@theme {
--ease-soft-out: cubic-bezier(0.22, 0.61, 0.36, 1);
--ease-soft-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--animate-rise: rise 700ms var(--ease-soft-out) backwards;
--animate-fade: fade 600ms var(--ease-soft-out) backwards;
@keyframes rise {
from { opacity: 0; transform: translateY(12px); }
}
@keyframes fade {
from { opacity: 0; }
}
}
- That gives
ease-soft-out,animate-riseandanimate-fadeas utilities:transition-colors duration-150 ease-soft-out,motion-safe:animate-rise. motion-safe:on every animation and every transform transition;motion-reduce:transition-noneormotion-reduce:animate-nonewhere a component library adds its own.- A revealed block's children:
groupon the block, thenmotion-safe:group-data-[shown]:animate-riseon the child. The hidden pre-state and the arrival delay are clearer as the dozen lines of CSS inmotion-revealin the global stylesheet than as long utility strings on every element. - Version 3: the same tokens in
theme.extend(transitionTimingFunction,keyframes,animation) intailwind.config;motion-safe:andmotion-reduce:are the same. - shadcn/ui's overlays use
tw-animate-css(formerlytailwindcss-animate):data-[state=open]:animate-in fade-in-0 zoom-in-95,data-[state=closed]:animate-out fade-out-0 zoom-out-95, withduration-200. Keep the distances small (slide-in-from-top-2) and leave its defaults when they already matchmotion-interface.
3. React
- The reveal hook sets the flag before paint and starts the one observer:
export function useReveal(page: RefObject<HTMLElement | null>) {
useLayoutEffect(() => {
const root = page.current;
return root ? startReveal(root) : undefined; // motion-reveal, section 2
}, [page]);
}
useLayoutEffect, not useEffect, for anything that sets a hidden
pre-state: it runs before the browser paints.
- Per-frame values never pass through the state of a large tree. A clock
that ticks ten times a second (a scripted demo,
motion-demo) lives in the small component that shows it; a parent that needs to follow along changes state only when something it shows changes (a phase, a few times per loop), through a callback in an effect. - Continuous values (the pointer, a drag, a progress) go into refs or CSS
variables on an element (
el.style.setProperty("--x", ...)), never intosetStatesixty times a second. - Replay from an event handler with
getAnimations()on a ref. - Changing a
keyremounts and replays mount animations: use it on purpose, never by accident in a list. - Strict mode runs effects twice in development: every observer disconnects in its cleanup.
The Motion library (motion, formerly Framer Motion)
- It earns its weight for: exits of components React removes
(
AnimatePresence), layout animation (an element changing size or place smoothly,layout), shared elements between views (layoutId), and gestures with springs (drag with momentum). Not for fades and reveals that CSS does. - Size: import from
motion/react, wrap the app inLazyMotionwithdomAnimation(domMaxonly for layout and drag) and usem.div, notmotion.div. That keeps it to a fraction of the full component's size. - Reduced motion once, at the root:
<MotionConfig reducedMotion="user">turns off transform and layout animation and keeps opacity;useReducedMotion()for choices in code. - Reveals:
whileInViewwithviewport={{ once: true, margin: "0px 0px -10% 0px" }}andinitial={{ opacity: 0, y: 12 }}. Theinitialstate is rendered on the server, so that content is hidden until the page hydrates: give content above the foldinitial={false}, and accept that hidden text below it is invisible without JavaScript, or use the CSS reveal instead. - Exits:
<AnimatePresence>around conditionally rendered children, withexit={{ opacity: 0 }};mode="wait"when one view replaces another. - Timing from the tokens, kept in one object that mirrors the CSS:
transition={{ duration: 0.24, ease: [0.22, 0.61, 0.36, 1] }}. Springs for drags and layout, critically damped so nothing overshoots:{ type: "spring", stiffness: 400, damping: 40 }.
Other libraries
- GSAP for a long scripted timeline with many parts, with
gsap.matchMedia()for reduced motion; ScrollTrigger only when CSS scroll-driven animations cannot do the job. - Lottie for a designer-made animation, with the lighter dotLottie player
rather than the full
lottie-web, and the first or last frame shown under reduced motion. - Each is a cost you state in the report (
motion-performance).
4. Next.js
- Motion lives in client components; keep the
"use client"boundary to the animated part (aRevealwrapper, the demo), not the whole page. - Server-rendered markup paints before hydration. A hidden pre-state needs
the inline head script with its safety timeout (
motion-reveal, section 3), written into<head>inapp/layout.tsxwithdangerouslySetInnerHTML, andsuppressHydrationWarningon<html>, since the attribute differs from the server's. - Route transitions: the View Transitions API where the version supports
it (recent versions have an experimental view transitions flag used with
React's
ViewTransitioncomponent), ordocument.startViewTransitionaround the navigation. No fade on every route. next/fontso a font swap does not move text in the middle of an entrance;next/imagewith sizes so an image does not shift a block while it reveals.loading.tsxshows only when the wait is worth showing (motion-interface, loading).
5. Vue
<Transition name="fade">with:
.fade-enter-active,
.fade-leave-active { transition: opacity var(--m-base) var(--m-ease-out); }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
and appear to play on the first render.
<TransitionGroup tag="ul" name="list">for lists, with.list-move { transition: transform var(--m-base) var(--m-ease-out); }for reordering and.list-leave-active { position: absolute; }so the others close the gap.- Reduced motion:
usePreferredReducedMotion()from VueUse, with anonetransition name when it isreduce, or the transition classes inside the no-preference media query. - A
v-revealdirective that adds its element to the page's one observer. - Nuxt: short
pageTransitionandlayoutTransition; the reveal flag from an inline head script throughuseHead.
6. Svelte (5)
transition:fade={{ duration: 240 }},in:fly={{ y: 12, duration: 240, easing: cubicOut }},out:fade={{ duration: 160 }}, andanimate:flip={{ duration: 240 }}inside a keyed{#each}for reordering.- Custom transitions return
css: (t) => ..., so they run as CSS rather than per-frame JavaScript. - Reduced motion:
import { prefersReducedMotion } from "svelte/motion", thenduration: prefersReducedMotion.current ? 0 : 240. TweenandSpringfromsvelte/motionfor values that animate (a number, a position):new Tween(0, { duration: 400, easing: cubicOut }).{#key value}replays intro transitions when the value changes: only on purpose.- SvelteKit:
onNavigatewithdocument.startViewTransitionfor route transitions.
7. Native mobile, briefly
- The platform's own navigation and sheet transitions win over web habits.
- React Native: Reanimated, which runs on the UI thread.
withTiming(value, { duration: 240, easing: Easing.bezier(0.22, 0.61, 0.36, 1), reduceMotion: ReduceMotion.System });useReducedMotion()for choices; layout animations such asentering={FadeIn.duration(240).reduceMotion(ReduceMotion.System)}. - Flutter: implicit animations (
AnimatedOpacity,AnimatedContainer,AnimatedSwitcher) for changes of state,Curves.easeOutCubicclose to the tokens, andMediaQuery.disableAnimationsOf(context)to set durations to zero. - SwiftUI:
withAnimation(.easeOut(duration: 0.24)), transitions such as.opacity.combined(with: .move(edge: .bottom)), and@Environment(\.accessibilityReduceMotion)to swap movement for a fade. - Jetpack Compose:
animate*AsState,AnimatedVisibilitywithfadeIn( tween(240)),updateTransition. Compose follows the system's animator duration scale, including "Remove animations"; a custom loop reads that setting itself.
Check it
- Search the code for
transition: all,transition-all,animate-bounceandanimate-pingused as decoration,motion.components withoutLazyMotion, andsetStateinside animation loops. previewwithmotion: trueafter building: the timeline, the animations driven from JavaScript, and the reduced-motion pass.- The bundle before and after, from the build's size report: the motion of a page should cost a few kilobytes, not a library.
Avoid
A motion library for fades; motion.div without LazyMotion; initial
hiding server-rendered text; transition-all; animate-bounce and
animate-ping as decoration; setState every frame; reduced motion
handled in each component instead of once; web-style transitions in a
native app where the platform has its own.
04 同仓库更多 Skills
demo-skill
Project-scope demo skill for tests.
writing
Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.
ui
Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.
ui-themes
Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.
ui-structure
Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.
ui-stack-vue
Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.