Motion-Primitives
Motion-primitives is a copy-in-source animation component library (same
philosophy as shadcn/ui) built on Motion for React (motion, the successor
to framer-motion) and Tailwind. 33 components across six categories: core
containers, text effects, number effects, interactive elements, toolbars, and
advanced effects. MIT licensed, by @ibelick.
Why it's good: it occupies the layer shadcn deliberately doesn't — the
expressive/delight layer. shadcn gives you accessible structure; motion-primitives
gives you the animated moments (a headline that reveals, a KPI that rolls, a
thumbnail that morphs into a fullscreen preview). Because components are copied
into your repo, they're editable and there's no runtime dependency beyond
motion itself — no version treadmill, no lock-in.
Setup (once per project)
Prerequisites: Tailwind, then:
npm install motion lucide-react
# lib/utils.ts must exist with the cn() helper (clsx + tailwind-merge)
Add components one at a time — either CLI works, both copy source into
components/motion-primitives/ (path configurable):
npx motion-primitives@latest add text-effect
# or via shadcn CLI:
npx shadcn@latest add "https://motion-primitives.com/c/text-effect.json"
If CLI/network is unavailable, copy the component file directly from
github.com/ibelick/motion-primitives → components/core/<name>.tsx. Imports
use motion/react (not framer-motion).
Per-component dependencies (verified by compile): the CLI resolves these
automatically, but manual copies must add them: infinite-slider and
sliding-number need react-use-measure; morphing-dialog needs
lucide-react and the repo's hooks/useClickOutside.tsx (copy it to
hooks/). If a copied component fails to resolve an import, check the repo's
hooks/ directory before assuming it's an npm package.
Workflow
- Decide whether motion belongs here at all (see "Where it makes sense"
below). If the surface is a dense work queue, the answer is usually no.
- Pick the component from the decision table below.
- Read
references/component-api.md for that component's exact props —
never write props from memory. Several props are non-obvious (e.g. Tilt's
reverse prop is spelled isRevese in the shipped source; most text
components accept children: string only, not ReactNode).
- Read
references/recipes.md if the task matches a recipe (hero reveal,
KPI ticker, asset-morph preview, marquee, streaming-text shimmer).
- Compose, don't stack. One motion idea per screen region. Two animated
text effects next to each other read as broken, not polished.
Where it makes sense
| Surface |
Verdict |
Reach for |
| Landing pages, hero sections, App Store listings |
Yes — primary habitat |
TextEffect, AnimatedGroup, InView, InfiniteSlider, Spotlight |
| Dashboard KPI tiles, stat counters |
Yes |
AnimatedNumber, SlidingNumber, TextShimmer for loading |
| Empty states, onboarding steps, first-run |
Yes |
TextEffect, TransitionPanel, AnimatedGroup |
| Asset/media preview (grid → fullscreen) |
Yes |
MorphingDialog, MorphingPopover, ImageComparison |
| AI streaming / "thinking" states |
Yes |
TextShimmer, TextShimmerWave, TextLoop |
| Marketing social proof, logo walls |
Yes |
InfiniteSlider, InView |
| Dense admin tables, review queues |
Mostly no — motion taxes throughput. At most: a subtle exit animation on approve/reject. |
|
| Forms, settings pages |
No — nothing to earn. |
|
| High-frequency live data (updates >1/sec) |
No — springs never settle; numbers become unreadable. Snap values instead. |
|
Anywhere prefers-reduced-motion users land |
Respect it: gate decorative motion behind the media query. Number/text content changes may remain. |
|
The test: motion should either explain a state change (this thing appeared /
moved / completed) or reward a moment that matters (first load, launch,
success). Motion that does neither is noise.
Decision table — task → component
| You need |
Component |
| Headline/paragraph reveal on load or scroll |
text-effect (presets: blur, scale, fade, slide; per word/char/line) |
| Rotating word in a headline ("build apps/agents/pipelines") |
text-loop |
| Loading/streaming text shimmer |
text-shimmer or text-shimmer-wave |
| Hacker-style character scramble |
text-scramble |
| Smooth text swap when a label changes |
text-morph |
| Per-letter rolling flip |
text-roll |
| Animated stat/KPI value |
animated-number (spring) or sliding-number (odometer digits) |
| Stagger-in a group of cards/items |
animated-group |
| Animate when scrolled into view |
in-view |
| Logo/testimonial marquee |
infinite-slider |
| Segmented control / tab hover highlight that follows the pointer |
animated-background |
| Multi-step panel transitions (wizards, onboarding) |
transition-panel |
| Thumbnail that morphs into a fullscreen dialog |
morphing-dialog |
| Button that morphs into a form/popover |
morphing-popover |
| Standard animated modal |
dialog |
| Expand/collapse single section |
disclosure; multiple sections: accordion |
| Swipeable/draggable carousel |
carousel |
| Before/after image slider |
image-comparison |
| macOS-style magnifying dock |
dock |
| Animated glow behind an element |
glow-effect; running light around a border: border-trail |
| Mouse-following spotlight on cards |
spotlight |
| 3D tilt on hover |
tilt |
| Element attracted to cursor |
magnetic |
| Custom cursor |
cursor |
| Circular rotating text badge |
spinning-text |
| Reading progress bar |
scroll-progress |
| Edge fade/blur on scrollable content |
progressive-blur |
Note on toolbars: toolbar-dynamic and toolbar-expandable are page-level
demos (default exports, hard-coded content), not composable primitives. Treat
them as reference implementations to adapt, not components to install and use
as-is.
Non-negotiables
- Every motion-primitives component is a client component — it or its parent
needs
'use client'. Keep the boundary at the leaf: animate the headline
component, not the whole page.
- Import from
motion/react, never framer-motion, in any code you write
alongside these components.
- Text-effect family takes
children: string. Passing JSX will type-error —
wrap mixed content by composing multiple instances instead.
- Don't animate layout-critical dimensions on containers with data-heavy
children; animate transform/opacity instead (the components already do this
internally — keep your customizations consistent with it).
- Check
references/component-api.md before writing props. If a prop you want
isn't listed, it doesn't exist — extend the copied source instead of guessing.
1---2name: motion-primitives3description: Add polished, pre-built animations to React/Next.js apps using the motion-primitives component library (motion-primitives.com) — animated text reveals, number tickers, morphing dialogs, marquees, scroll reveals, hover effects. Use this whenever a task involves animating UI, making a landing page or hero section feel alive, animating numbers/KPIs on a dashboard, adding entrance/reveal animations, marquee/logo sliders, animated dialogs or popovers, shimmer/loading text states, or when someone says an interface feels "static", "flat", or "needs polish". Also use it to decide when NOT to animate. Always consult references/component-api.md for exact prop names before writing any motion-primitives code — the APIs here are extracted from source and several differ from what you'd guess.4---56# Motion-Primitives78Motion-primitives is a copy-in-source animation component library (same9philosophy as shadcn/ui) built on **Motion for React** (`motion`, the successor10to framer-motion) and Tailwind. 33 components across six categories: core11containers, text effects, number effects, interactive elements, toolbars, and12advanced effects. MIT licensed, by @ibelick.1314**Why it's good:** it occupies the layer shadcn deliberately doesn't — the15expressive/delight layer. shadcn gives you accessible structure; motion-primitives16gives you the animated moments (a headline that reveals, a KPI that rolls, a17thumbnail that morphs into a fullscreen preview). Because components are copied18into your repo, they're editable and there's no runtime dependency beyond19`motion` itself — no version treadmill, no lock-in.2021## Setup (once per project)2223Prerequisites: Tailwind, then:2425```bash26npm install motion lucide-react27# lib/utils.ts must exist with the cn() helper (clsx + tailwind-merge)28```2930Add components one at a time — either CLI works, both copy source into31`components/motion-primitives/` (path configurable):3233```bash34npx motion-primitives@latest add text-effect35# or via shadcn CLI:36npx shadcn@latest add "https://motion-primitives.com/c/text-effect.json"37```3839If CLI/network is unavailable, copy the component file directly from40`github.com/ibelick/motion-primitives` → `components/core/<name>.tsx`. Imports41use `motion/react` (not `framer-motion`).4243**Per-component dependencies (verified by compile):** the CLI resolves these44automatically, but manual copies must add them: `infinite-slider` and45`sliding-number` need `react-use-measure`; `morphing-dialog` needs46`lucide-react` and the repo's `hooks/useClickOutside.tsx` (copy it to47`hooks/`). If a copied component fails to resolve an import, check the repo's48`hooks/` directory before assuming it's an npm package.4950## Workflow51521. **Decide whether motion belongs here at all** (see "Where it makes sense"53 below). If the surface is a dense work queue, the answer is usually no.542. **Pick the component** from the decision table below.553. **Read `references/component-api.md`** for that component's exact props —56 never write props from memory. Several props are non-obvious (e.g. Tilt's57 reverse prop is spelled `isRevese` in the shipped source; most text58 components accept `children: string` only, not ReactNode).594. **Read `references/recipes.md`** if the task matches a recipe (hero reveal,60 KPI ticker, asset-morph preview, marquee, streaming-text shimmer).615. **Compose, don't stack.** One motion idea per screen region. Two animated62 text effects next to each other read as broken, not polished.6364## Where it makes sense6566| Surface | Verdict | Reach for |67|---|---|---|68| Landing pages, hero sections, App Store listings | **Yes — primary habitat** | TextEffect, AnimatedGroup, InView, InfiniteSlider, Spotlight |69| Dashboard KPI tiles, stat counters | **Yes** | AnimatedNumber, SlidingNumber, TextShimmer for loading |70| Empty states, onboarding steps, first-run | **Yes** | TextEffect, TransitionPanel, AnimatedGroup |71| Asset/media preview (grid → fullscreen) | **Yes** | MorphingDialog, MorphingPopover, ImageComparison |72| AI streaming / "thinking" states | **Yes** | TextShimmer, TextShimmerWave, TextLoop |73| Marketing social proof, logo walls | **Yes** | InfiniteSlider, InView |74| Dense admin tables, review queues | **Mostly no** — motion taxes throughput. At most: a subtle exit animation on approve/reject. |75| Forms, settings pages | **No** — nothing to earn. |76| High-frequency live data (updates >1/sec) | **No** — springs never settle; numbers become unreadable. Snap values instead. |77| Anywhere `prefers-reduced-motion` users land | Respect it: gate decorative motion behind the media query. Number/text *content* changes may remain. |7879The test: motion should either **explain a state change** (this thing appeared /80moved / completed) or **reward a moment that matters** (first load, launch,81success). Motion that does neither is noise.8283## Decision table — task → component8485| You need | Component |86|---|---|87| Headline/paragraph reveal on load or scroll | `text-effect` (presets: blur, scale, fade, slide; per word/char/line) |88| Rotating word in a headline ("build **apps/agents/pipelines**") | `text-loop` |89| Loading/streaming text shimmer | `text-shimmer` or `text-shimmer-wave` |90| Hacker-style character scramble | `text-scramble` |91| Smooth text swap when a label changes | `text-morph` |92| Per-letter rolling flip | `text-roll` |93| Animated stat/KPI value | `animated-number` (spring) or `sliding-number` (odometer digits) |94| Stagger-in a group of cards/items | `animated-group` |95| Animate when scrolled into view | `in-view` |96| Logo/testimonial marquee | `infinite-slider` |97| Segmented control / tab hover highlight that follows the pointer | `animated-background` |98| Multi-step panel transitions (wizards, onboarding) | `transition-panel` |99| Thumbnail that morphs into a fullscreen dialog | `morphing-dialog` |100| Button that morphs into a form/popover | `morphing-popover` |101| Standard animated modal | `dialog` |102| Expand/collapse single section | `disclosure`; multiple sections: `accordion` |103| Swipeable/draggable carousel | `carousel` |104| Before/after image slider | `image-comparison` |105| macOS-style magnifying dock | `dock` |106| Animated glow behind an element | `glow-effect`; running light around a border: `border-trail` |107| Mouse-following spotlight on cards | `spotlight` |108| 3D tilt on hover | `tilt` |109| Element attracted to cursor | `magnetic` |110| Custom cursor | `cursor` |111| Circular rotating text badge | `spinning-text` |112| Reading progress bar | `scroll-progress` |113| Edge fade/blur on scrollable content | `progressive-blur` |114115**Note on toolbars:** `toolbar-dynamic` and `toolbar-expandable` are page-level116demos (default exports, hard-coded content), not composable primitives. Treat117them as reference implementations to adapt, not components to install and use118as-is.119120## Non-negotiables121122- Every motion-primitives component is a client component — it or its parent123 needs `'use client'`. Keep the boundary at the leaf: animate the headline124 component, not the whole page.125- Import from `motion/react`, never `framer-motion`, in any code you write126 alongside these components.127- Text-effect family takes `children: string`. Passing JSX will type-error —128 wrap mixed content by composing multiple instances instead.129- Don't animate layout-critical dimensions on containers with data-heavy130 children; animate transform/opacity instead (the components already do this131 internally — keep your customizations consistent with it).132- Check `references/component-api.md` before writing props. If a prop you want133 isn't listed, it doesn't exist — extend the copied source instead of guessing.