feat: Expand fleet to 23 skills across all domains
New skills (14): - nestjs-best-practices: 40 priority-ranked rules (kadajett) - fastapi: Pydantic v2, async SQLAlchemy, JWT auth (jezweb) - architecture-patterns: Clean Architecture, Hexagonal, DDD (wshobson) - python-performance-optimization: Profiling and optimization (wshobson) - ai-sdk: Vercel AI SDK streaming and agent patterns (vercel) - create-agent: Modular agent architecture with OpenRouter (openrouterteam) - proactive-agent: WAL Protocol, compaction recovery, self-improvement (halthelobster) - brand-guidelines: Brand identity enforcement (anthropics) - ui-animation: Motion design with accessibility (mblode) - marketing-ideas: 139 ideas across 14 categories (coreyhaines31) - pricing-strategy: SaaS pricing and tier design (coreyhaines31) - programmatic-seo: SEO at scale with playbooks (coreyhaines31) - competitor-alternatives: Comparison page architecture (coreyhaines31) - referral-program: Referral and affiliate programs (coreyhaines31) README reorganized by domain: Code Quality, Frontend, Backend, Auth, AI/Agent Building, Marketing, Design, Meta. Mosaic Stack is not limited to coding — the Orchestrator serves coding, business, design, marketing, writing, logistics, and analysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
skills/ui-animation/SKILL.md
Normal file
51
skills/ui-animation/SKILL.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: ui-animation
|
||||
description: Guidelines and examples for UI motion and animation. Use when designing, implementing, or reviewing motion, easing, timing, reduced-motion behaviour, CSS transitions, keyframes, framer-motion, or spring animations.
|
||||
---
|
||||
|
||||
# UI Animation
|
||||
|
||||
## Core rules
|
||||
- Animate to clarify cause/effect or add deliberate delight.
|
||||
- Keep interactions fast (200-300ms; up to 1s only for illustrative motion).
|
||||
- Never animate keyboard interactions (arrow-key navigation, shortcut responses, tab/focus).
|
||||
- Prefer CSS; use WAAPI or JS only when needed.
|
||||
- Make animations interruptible and input-driven.
|
||||
- Honor `prefers-reduced-motion` (reduce or disable).
|
||||
|
||||
## What to animate
|
||||
- For movement and spatial change, animate only `transform` and `opacity`.
|
||||
- For simple state feedback, `color`, `background-color`, and `opacity` transitions are acceptable.
|
||||
- Never animate layout properties; never use `transition: all`.
|
||||
- Avoid `filter` animation for core interactions; if unavoidable, keep blur <= 20px.
|
||||
- SVG: apply transforms on a `<g>` wrapper with `transform-box: fill-box; transform-origin: center`.
|
||||
- Disable transitions during theme switches.
|
||||
|
||||
## Spatial and sequencing
|
||||
- Set `transform-origin` at the trigger point.
|
||||
- For dialogs/menus, start around `scale(0.85-0.9)`; avoid `scale(0)`.
|
||||
- Stagger reveals <= 50ms.
|
||||
|
||||
## Easing defaults
|
||||
- Enter and transform-based hover: `cubic-bezier(0.22, 1, 0.36, 1)`.
|
||||
- Move: `cubic-bezier(0.25, 1, 0.5, 1)`.
|
||||
- Simple hover colour/background/opacity: `200ms ease`.
|
||||
- Avoid `ease-in` for UI (feels slow).
|
||||
|
||||
## Accessibility
|
||||
- If `transform` is used, disable it in `prefers-reduced-motion`.
|
||||
- Disable hover transitions on touch devices via `@media (hover: hover) and (pointer: fine)`.
|
||||
|
||||
## Performance
|
||||
- Pause looping animations off-screen.
|
||||
- Toggle `will-change` only during heavy motion and only for `transform`/`opacity`.
|
||||
- Prefer `transform` over positional props in animation libraries.
|
||||
- Do not animate drag gestures using CSS variables.
|
||||
|
||||
## Reference
|
||||
- Snippets and practical tips: [examples.md](examples.md)
|
||||
|
||||
## Workflow
|
||||
1. Start with the core rules, then pick a reference snippet from [examples.md](examples.md).
|
||||
2. Keep motion functional; honor `prefers-reduced-motion`.
|
||||
3. When reviewing, cite file paths and line numbers and propose concrete fixes.
|
||||
239
skills/ui-animation/examples.md
Normal file
239
skills/ui-animation/examples.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# UI Animation Examples
|
||||
|
||||
Snippets and tips for the core rules in `SKILL.md`.
|
||||
|
||||
## Table of contents
|
||||
- [Enter and exit](#enter-and-exit)
|
||||
- [Spatial rules and stagger](#spatial-rules-and-stagger)
|
||||
- [Drawer (move easing)](#drawer-move-easing)
|
||||
- [Hover transitions](#hover-transitions)
|
||||
- [Reduced motion](#reduced-motion)
|
||||
- [Origin-aware animations](#origin-aware-animations)
|
||||
- [Performance recipes](#performance-recipes)
|
||||
- [Practical tips](#practical-tips)
|
||||
|
||||
## Enter and exit
|
||||
```css
|
||||
/* Toast.module.css */
|
||||
.toast {
|
||||
transform: translate3d(0, 6px, 0);
|
||||
opacity: 0;
|
||||
transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
opacity 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.toast[data-open="true"] {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Disable transitions during theme switch */
|
||||
[data-theme-switching="true"] * {
|
||||
transition: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/components/Panel.tsx
|
||||
"use client";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
|
||||
export function Panel() {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={reduceMotion ? undefined : { scale: 1.02 }}
|
||||
whileTap={reduceMotion ? undefined : { scale: 0.98 }}
|
||||
transition={
|
||||
reduceMotion ? { duration: 0 } : { duration: 0.2, ease: [0.22, 1, 0.36, 1] }
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Spatial rules and stagger
|
||||
```css
|
||||
/* Menu.module.css */
|
||||
.menu {
|
||||
transform-origin: top right;
|
||||
transform: scale(0.88);
|
||||
opacity: 0;
|
||||
transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
opacity 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.menu[data-open="true"] {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.list > * {
|
||||
animation: fade-in 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
.list > *:nth-child(2) { animation-delay: 50ms; }
|
||||
.list > *:nth-child(3) { animation-delay: 100ms; }
|
||||
```
|
||||
|
||||
```tsx
|
||||
const listVariants = {
|
||||
show: { transition: { staggerChildren: 0.05 } },
|
||||
};
|
||||
```
|
||||
|
||||
## Drawer (move easing)
|
||||
```css
|
||||
.drawer {
|
||||
transition: transform 240ms cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<motion.aside
|
||||
initial={{ transform: "translate3d(100%, 0, 0)" }}
|
||||
animate={{ transform: "translate3d(0, 0, 0)" }}
|
||||
exit={{ transform: "translate3d(100%, 0, 0)" }}
|
||||
transition={{ duration: 0.24, ease: [0.25, 1, 0.5, 1] }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Hover transitions
|
||||
```css
|
||||
/* Link.module.css */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.link {
|
||||
transition: color 200ms ease, opacity 200ms ease;
|
||||
}
|
||||
.link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reduced motion
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu,
|
||||
.toast {
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
|
||||
export function AnimatedCard() {
|
||||
const reduceMotion = useReducedMotion();
|
||||
return (
|
||||
<motion.div
|
||||
animate={reduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}
|
||||
initial={reduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.98 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Origin-aware animations
|
||||
```css
|
||||
.popover[data-side="top"] { transform-origin: bottom center; }
|
||||
.popover[data-side="bottom"] { transform-origin: top center; }
|
||||
.popover[data-side="left"] { transform-origin: center right; }
|
||||
.popover[data-side="right"] { transform-origin: center left; }
|
||||
```
|
||||
|
||||
## Performance recipes
|
||||
|
||||
### Pause looping animations off-screen
|
||||
```ts
|
||||
// app/hooks/usePauseOffscreen.ts
|
||||
"use client";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function usePauseOffscreen<T extends HTMLElement>() {
|
||||
const ref = useRef<T | null>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const io = new IntersectionObserver(([entry]) => {
|
||||
el.style.animationPlayState = entry.isIntersecting ? "running" : "paused";
|
||||
});
|
||||
io.observe(el);
|
||||
return () => io.disconnect();
|
||||
}, []);
|
||||
return ref;
|
||||
}
|
||||
```
|
||||
|
||||
### Toggle will-change during animation
|
||||
```css
|
||||
.animating {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
```
|
||||
|
||||
### Spring defaults (framer-motion)
|
||||
```tsx
|
||||
<motion.div
|
||||
animate={{ transform: "translate3d(0, 0, 0)" }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 40 }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Practical tips
|
||||
|
||||
### Record your animations
|
||||
When something feels off, record the animation and play it back frame by frame.
|
||||
|
||||
### Fix shaky 1px shifts
|
||||
Elements can shift by 1px at the start/end of CSS transforms due to GPU/CPU handoff. Apply `will-change: transform` during the animation (not permanently) to keep compositing on the GPU.
|
||||
|
||||
### Scale buttons on press
|
||||
```css
|
||||
button:active {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.9;
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid animating from scale(0)
|
||||
```css
|
||||
.element {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
.element.visible {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
### Skip animation on subsequent tooltips
|
||||
```css
|
||||
.tooltip {
|
||||
transition:
|
||||
transform 125ms ease-out,
|
||||
opacity 125ms ease-out;
|
||||
transform-origin: var(--transform-origin);
|
||||
}
|
||||
.tooltip[data-starting-style],
|
||||
.tooltip[data-ending-style] {
|
||||
opacity: 0;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
.tooltip[data-instant] {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
```
|
||||
|
||||
### Fix hover flicker
|
||||
Apply the hover effect on a parent, animate the child:
|
||||
```css
|
||||
.box:hover .box-inner {
|
||||
transform: translateY(-20%);
|
||||
}
|
||||
.box-inner {
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user