bitjaru/ss-motion
Apply a named StyleSeed motion to a component — either one of the 5 personality seeds (Spring/Silk/Snap/Float/Pulse × entrance/exit/hover/press/layout) or a distinctive keyword move from the motion library (toggle-flip, toggle-curtain, reveal-blur, pop-in, shimmer, …). Translates vibe words into framer-motion code from one source of truth.
npx skills add https://github.com/bitjaru/styleseed --skill ss-motion
motion.X JSX onlyseeds/moves). Note these ARE allowed on a marketing/landing/brand page (DESIGN-LANGUAGE §43
Cinematic tier); just build them with a scroll library, not this skill. On app/data surfaces they
stay banned.
engine/components/ui/motion.tsx directlyTranslate the user's prompt to one of the five seeds before applying. Use this lookup table from engine/motion/index.ts:
| Words the user might say | Seed |
|---|---|
| bouncy, springy, playful, energetic, alive | Spring |
| smooth, silky, fluid, elegant, composed, continuous | Silk |
| snappy, quick, instant, decisive, sharp, precise | Snap |
| floaty, gentle, weightless, dreamy, ambient, drifting | Float |
| rhythmic, punchy, pulsing, heartbeat, beat | Pulse |
| "Toss style", "Arc style" | Spring (per brand default) |
| "Stripe style", "Notion style" | Silk |
| "Linear style", "Raycast style", "Vercel style" | Snap |
If the user says only a *brand* name, use that brand's default seed from BRAND_DEFAULT_SEED. If the user is explicit about a seed name (spring, silk, etc.), respect it verbatim.
If the user describes what the thing is ("a like button", "a modal", "the loading
state", "items in a feed") rather than a feeling, recommend from the use-case map
(MOTION_BY_USECASE in engine/motion/library.ts, exported from @engine/motion):
| Use case | Reach for | Why |
|---|---|---|
| Primary button / CTA press | spring · press | tactile, confident — the press should "give" |
| Modal / dialog / sheet enter | silk · entrance | smooth; never bounce serious/destructive content |
| Dropdown / popover / menu | snap · entrance | instant, precise — frequent UI shouldn't wait |
| Toast / inline notification | spring · entrance | small friendly arrival, non-blocking |
| List / feed items appearing | stagger-cascade | choreograph order, gently |
| Feature / marketing card hover | tilt-3d | depth/flair OK on content-light marketing |
| Dashboard / data card hover | snap · hover | a subtle lift only — keep dense UI calm |
| Like / favorite / reaction | like-burst | a celebratory one-shot; reward the tap |
| Live / online / recording dot | pulse-beat | looping heartbeat = "alive" |
| Loading / skeleton | shimmer | calm directional progress |
| Success / confirmation | pop-in | positive little "done" |
| Toggle / tab / segment switch | toggle-flip | distinctive, recognizable switch |
| Page / route transition | silk · entrance | smooth, minimal, get out of the way |
| Number / balance / KPI / price reveal | none | don't animate the payload — it must read instantly |
Two anti-rules override the table (state them if you deviate):
Seeds set a *personality* (how a fade/scale feels). The motion library in
engine/motion/library.ts adds *distinctive moves* — a flip, a curtain wipe, a
morph — each behind a unique keyword. Prefer a keyword when the user wants a
specific, recognizable motion rather than a generic feel.
engine/motion/library.ts (exported as MOTION_LIBRARY / MOTION_BY_KEY from
@engine/motion) is the single source of truth — every keyword carries its
own runnable snippet. Pull the snippet from there; never hand-write the params.
| Keyword | Move | Say it when the user wants… |
|---|---|---|
| toggle-flip | 3D Y-axis card flip | a switch/toggle to flip between two faces |
| toggle-slide | slide-stack swap | a value to slide out and the next to slide in |
| toggle-morph | pill ⇄ circle morph | a control to change shape on toggle |
| toggle-curtain | top→bottom clip-path wipe | a panel to reveal like a curtain |
| reveal-blur | blur(12px)→0 focus-in | content to focus-pull into place |
| reveal-rise | masked clip-path text rise | a headline/text to climb into view |
| reveal-unfold | scaleY from top edge | an accordion/panel to unfold |
| pop-in | spring overshoot from 0 | a badge/checkmark to pop in bouncily |
| press-squish | scale-down + skew | a button to feel jelly/tactile on tap |
| tap-ripple | radial ripple from tap | Material-style press feedback |
| pulse-beat | looping scale pulse | a live/recording/heartbeat indicator |
| wiggle | quick horizontal shake | error / invalid-input feedback |
| shimmer | skeleton loading sweep | a loading placeholder |
| stagger-cascade | children fade-up in sequence | a list to animate in one-by-one |
Applying a keyword:
engine/motion/library.ts — find the entry whosekey matches, copy its snippet verbatim (it is calibrated and runnable).
useState shown in thesnippet. If it's a one-shot reveal, a key bump replays it.
consistency, and point them at /motion to preview/Copy others.
If the user describes a move but no exact keyword fits, fall back to a seed +
context. If they say a keyword that doesn't exist, suggest the closest real one
from the table — never invent a keyword.
Infer one of the five contexts from the prompt:
hoverpressentranceexit (requires <AnimatePresence>)layoutIf ambiguous, default to entrance. If multiple contexts are reasonable (e.g., a button needs both hover and press), apply both.
Apply seed: $0 · Context: $1 · Target: $ARGUMENTS
<button>, <div>, <Card>, or similar.motion (and AnimatePresence for exit) from "framer-motion""@engine/motion" — in a project that doesn't use the @engine/* alias, use a relative path to engine/motion<motion.X> and spread the seed's recipe: // hover example
<motion.button {...spring.hover}>Save</motion.button>
// press + hover combined
<motion.button {...spring.press} {...spring.hover}>Save</motion.button>
// entrance (mount)
<motion.div {...silk.entrance}>...</motion.div>
// exit (requires AnimatePresence wrapper somewhere up the tree)
<AnimatePresence>
{open && <motion.div {...silk.entrance} {...silk.exit} />}
</AnimatePresence>
// layout (FLIP)
<motion.div {...snap.layout}>...</motion.div>
{ type: "spring", stiffness: 300, damping: 18 } into the JSX — always spread the recipe.prefers-reduced-motion in long-running surfaces. For one-off interactions (hover/press), framer-motion already throttles. For mount/exit/layout sequences in a long-lived page, import usePrefersReducedMotion and REDUCED_TRANSITION from @engine/motion and override the transition when reduced motion is on.exit was used).press too so it feels clickable?").pressengine/motion/seeds/*.ts from this skill — those are calibrated by hand. Add a new seed only via a separate, explicit ask.per-component seeds/moves. (Scroll-linked/parallax/3D ARE allowed on a marketing/landing/brand
page per DESIGN-LANGUAGE §43 Cinematic tier — build those directly with a scroll lib; on
app/data surfaces they stay forbidden.) Infinite loops remain banned everywhere except skeleton pulse.
Take bitjaru/ss-motion from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.