mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 00:56:30 +03:00
The public repo keeps the OSS promise surface: skill, CLI, extension, tests, and the provider build. The site, labs, concept/composition catalogs, image pipeline, Cloudflare functions, and authoring guide move to pbakaus/impeccable-site. concept-seed tests run against a synthetic fixture catalog; the plugin icon and skill categories moved in-repo; build validation narrows to README prose and non-site counts; release notes read from a sibling impeccable-site checkout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
557 lines
29 KiB
HTML
557 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>/impeccable animate | Impeccable</title>
|
||
<meta name="description" content="Purposeful motion that conveys state, not decoration.">
|
||
<meta name="theme-color" content="#fafafa">
|
||
<link rel="canonical" href="https://impeccable.style/docs/animate">
|
||
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="../css/sub-pages.css">
|
||
|
||
</head>
|
||
<body class="sub-page skills-layout-page">
|
||
<a href="#main" class="skip-link">Skip to content</a>
|
||
<!-- site-header v1 -->
|
||
<header class="site-header" data-site-header>
|
||
<a href="/" class="site-header-brand" aria-label="Impeccable home">
|
||
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
|
||
<span class="site-header-brand-name">Impeccable</span>
|
||
</a>
|
||
<div class="site-header-right">
|
||
<nav class="site-header-nav" aria-label="Primary">
|
||
<a href="/" data-nav="home">Home</a>
|
||
<a href="/designing" data-nav="designing">Designing</a>
|
||
<a href="/docs" data-nav="docs" aria-current="page">Docs</a>
|
||
<a href="/slop" data-nav="slop">Slop</a>
|
||
<a href="/live-mode" data-nav="live">Live</a>
|
||
</nav>
|
||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||
<span class="site-header-github-label">21k</span>
|
||
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
|
||
</a>
|
||
</div>
|
||
</header>
|
||
<main id="main">
|
||
|
||
<div class="skills-layout">
|
||
|
||
<aside class="skills-sidebar" aria-label="Documentation">
|
||
<button class="skills-sidebar-toggle" type="button" aria-expanded="false" aria-controls="skills-sidebar-inner">
|
||
<span class="skills-sidebar-toggle-label">/animate</span>
|
||
<svg class="skills-sidebar-toggle-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
||
</button>
|
||
<div class="skills-sidebar-inner" id="skills-sidebar-inner">
|
||
<p class="skills-sidebar-label">Docs</p>
|
||
|
||
<div class="skills-sidebar-group" data-category="tutorials">
|
||
<p class="skills-sidebar-group-title">Tutorials</p>
|
||
<ul class="skills-sidebar-list">
|
||
<li><a href="/tutorials/getting-started">Getting started</a></li>
|
||
<li><a href="/tutorials/iterate-live">Iterate on UI with Live Mode</a></li>
|
||
<li><a href="/tutorials/brand-vs-product">Brand vs product, pick a register</a></li>
|
||
<li><a href="/tutorials/critique-with-overlay">Critique with the visual overlay</a></li>
|
||
</ul>
|
||
</div>
|
||
<hr class="skills-sidebar-divider">
|
||
|
||
<div class="skills-sidebar-group" data-category="create">
|
||
<p class="skills-sidebar-group-title">Create</p>
|
||
<ul class="skills-sidebar-list">
|
||
<li><a href="/docs/impeccable">/impeccable</a></li>
|
||
<li><a href="/docs/craft">craft</a></li>
|
||
<li><a href="/docs/shape">shape</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="skills-sidebar-group" data-category="evaluate">
|
||
<p class="skills-sidebar-group-title">Evaluate</p>
|
||
<ul class="skills-sidebar-list">
|
||
<li><a href="/docs/audit">audit</a></li>
|
||
<li><a href="/docs/critique">critique</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="skills-sidebar-group" data-category="refine">
|
||
<p class="skills-sidebar-group-title">Refine</p>
|
||
<ul class="skills-sidebar-list">
|
||
<li><a href="/docs/animate" aria-current="page">animate</a></li>
|
||
<li><a href="/docs/bolder">bolder</a></li>
|
||
<li><a href="/docs/colorize">colorize</a></li>
|
||
<li><a href="/docs/delight">delight</a></li>
|
||
<li><a href="/docs/layout">layout</a></li>
|
||
<li><a href="/docs/overdrive">overdrive</a></li>
|
||
<li><a href="/docs/quieter">quieter</a></li>
|
||
<li><a href="/docs/typeset">typeset</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="skills-sidebar-group" data-category="simplify">
|
||
<p class="skills-sidebar-group-title">Simplify</p>
|
||
<ul class="skills-sidebar-list">
|
||
<li><a href="/docs/adapt">adapt</a></li>
|
||
<li><a href="/docs/clarify">clarify</a></li>
|
||
<li><a href="/docs/distill">distill</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="skills-sidebar-group" data-category="harden">
|
||
<p class="skills-sidebar-group-title">Harden</p>
|
||
<ul class="skills-sidebar-list">
|
||
<li><a href="/docs/harden">harden</a></li>
|
||
<li><a href="/docs/onboard">onboard</a></li>
|
||
<li><a href="/docs/optimize">optimize</a></li>
|
||
<li><a href="/docs/polish">polish</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="skills-sidebar-group" data-category="system">
|
||
<p class="skills-sidebar-group-title">System</p>
|
||
<ul class="skills-sidebar-list">
|
||
<li><a href="/docs/document">document</a></li>
|
||
<li><a href="/docs/extract">extract</a></li>
|
||
<li><a href="/docs/live">live</a></li>
|
||
<li><a href="/docs/teach">teach</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
</aside>
|
||
<div class="skills-main">
|
||
|
||
<article class="skill-detail">
|
||
<div class="skill-detail-hero skill-detail-hero--has-demo">
|
||
<header class="skill-detail-header">
|
||
<p class="skill-detail-eyebrow"><a href="/docs">Docs</a> / Refine</p>
|
||
<h1 class="skill-detail-title"><span class="skill-detail-title-namespace"><span class="skill-detail-title-slash">/</span>impeccable</span>animate</h1>
|
||
<p class="skill-detail-tagline">Purposeful motion that conveys state, not decoration.</p>
|
||
|
||
<div class="skill-meta-strip">
|
||
<span class="skill-meta-chip skill-meta-category" data-category="refine">Refine</span>
|
||
<span class="skill-meta-chip">User-invocable</span>
|
||
<span class="skill-meta-chip skill-meta-args">[target]</span>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="skill-demo" aria-label="Before and after demo">
|
||
<div class="split-comparison" data-demo="skill-animate">
|
||
<p class="skill-demo-eyebrow">Drag or hover to compare</p>
|
||
<div class="split-container">
|
||
<div class="split-before">
|
||
<div class="split-content">
|
||
<div style="width: 100%; max-width: 220px; display: flex; flex-direction: column; gap: 8px;">
|
||
<div style="height: 32px; background: #e0e0e0; border-radius: 4px;"></div>
|
||
<div style="height: 12px; background: #e0e0e0; border-radius: 2px; width: 60%;"></div>
|
||
<div style="display: flex; gap: 8px; margin-top: 8px;">
|
||
<div style="flex: 1; height: 64px; background: #e0e0e0; border-radius: 4px;"></div>
|
||
<div style="flex: 1; height: 64px; background: #e0e0e0; border-radius: 4px;"></div>
|
||
</div>
|
||
<div style="height: 10px; background: #e0e0e0; border-radius: 2px; width: 80%; margin-top: 8px;"></div>
|
||
<div style="height: 10px; background: #e0e0e0; border-radius: 2px; width: 65%;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="split-after">
|
||
<div class="split-content">
|
||
<div style="width: 100%; max-width: 220px; display: flex; flex-direction: column; gap: 8px;">
|
||
<div style="height: 32px; background: var(--color-ink); border-radius: 4px; animation: animDemoFade 0.5s ease-out both;"></div>
|
||
<div style="height: 12px; background: var(--color-ash); border-radius: 2px; width: 60%; animation: animDemoFade 0.5s ease-out 0.1s both;"></div>
|
||
<div style="display: flex; gap: 8px; margin-top: 8px;">
|
||
<div style="flex: 1; height: 64px; background: var(--color-mist); border-radius: 4px; animation: animDemoSlide 0.4s ease-out 0.2s both;"></div>
|
||
<div style="flex: 1; height: 64px; background: var(--color-mist); border-radius: 4px; animation: animDemoSlide 0.4s ease-out 0.3s both;"></div>
|
||
</div>
|
||
<div style="height: 10px; background: var(--color-mist); border-radius: 2px; width: 80%; margin-top: 8px; animation: animDemoFade 0.4s ease-out 0.4s both;"></div>
|
||
<div style="height: 10px; background: var(--color-mist); border-radius: 2px; width: 65%; animation: animDemoFade 0.4s ease-out 0.5s both;"></div>
|
||
</div>
|
||
<style>
|
||
@keyframes animDemoFade {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
@keyframes animDemoSlide {
|
||
from { opacity: 0; transform: translateY(16px) scale(0.95); }
|
||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||
}
|
||
</style>
|
||
</div>
|
||
</div>
|
||
<div class="split-divider"></div>
|
||
</div>
|
||
<div class="split-labels">
|
||
<span class="split-label-item" data-point="before">Before</span>
|
||
<p class="skill-demo-caption">Static layout → Choreographed entrance</p>
|
||
<span class="split-label-item" data-point="after">After</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<section class="skill-detail-editorial prose">
|
||
<h2 id="when-to-use-it">When to use it</h2>
|
||
<p><code>/impeccable animate</code> is for interfaces that feel lifeless, where state changes are instant and jarring, where loading just pops in, where the user never quite trusts that their click registered. Use it to add the small motions that communicate what is happening: entrances, exits, feedback, transitions between states.</p>
|
||
<p>Do not use it to add bounces or elastic springs for the sake of energy. That is decoration, and this skill will not give it to you.</p>
|
||
<h2 id="how-it-works">How it works</h2>
|
||
<p>The skill identifies static moments that would benefit from motion, then applies them with strict discipline:</p>
|
||
<ol>
|
||
<li><strong>Entrances and exits</strong>: elements appear and leave with 200 to 300ms fades plus subtle Y or scale, never layout properties.</li>
|
||
<li><strong>State feedback</strong>: hover, active, focus, loading, success all communicate via motion instead of sudden swaps.</li>
|
||
<li><strong>Transitions between views</strong>: shared-element transitions where it makes sense, fade-through otherwise.</li>
|
||
<li><strong>Progress and loading</strong>: skeleton screens, determinate bars, motion that says "still working".</li>
|
||
<li><strong>Reduced motion</strong>: every animation has a <code>prefers-reduced-motion</code> fallback.</li>
|
||
</ol>
|
||
<p>Easing is always exponential (ease-out-quart, quint, or expo) because real objects decelerate smoothly. No bounce, no elastic, no linear for anything except progress indicators.</p>
|
||
<p>The skill animates <code>transform</code> and <code>opacity</code> only. If you find yourself animating <code>width</code>, <code>height</code>, <code>top</code>, or <code>left</code>, it is doing the wrong thing. Use <code>grid-template-rows</code> for height transitions.</p>
|
||
<h2 id="try-it">Try it</h2>
|
||
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable animate the sign-up flow</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable animate the sign-up flow" aria-label="Copy to clipboard"></button></div>
|
||
<p>Typical additions:</p>
|
||
<ul>
|
||
<li>Email input gets a focus glow on focus-visible (opacity + shadow, 180ms)</li>
|
||
<li>Submit button shows a spinner inside itself on loading state, not a separate spinner next to it</li>
|
||
<li>Success screen enters with opacity + translateY(8px), 260ms, ease-out-quart</li>
|
||
<li>Error message slides down with grid-template-rows (not height), 220ms</li>
|
||
<li><code>@media (prefers-reduced-motion: reduce)</code> fallback for every transition</li>
|
||
</ul>
|
||
<h2 id="pitfalls">Pitfalls</h2>
|
||
<ul>
|
||
<li><strong>Asking for "more animation".</strong> Animate is not a dial. It adds where motion communicates, not everywhere.</li>
|
||
<li><strong>Removing the reduced-motion fallbacks.</strong> The skill adds them automatically. Non-negotiable for accessibility.</li>
|
||
</ul>
|
||
|
||
</section>
|
||
|
||
|
||
|
||
<section class="skill-source-card">
|
||
<header class="skill-source-card-header">
|
||
<span class="skill-source-card-label">reference/animate.md</span>
|
||
<span class="skill-source-card-subtitle">Loaded when the impeccable skill routes to this command.</span>
|
||
</header>
|
||
<div class="skill-source-card-body prose">
|
||
<blockquote>
|
||
<p><strong>Additional context needed</strong>: performance constraints.</p>
|
||
</blockquote>
|
||
<p>Analyze a feature and strategically add animations and micro-interactions that enhance understanding, provide feedback, and create delight.</p>
|
||
<hr>
|
||
<h2 id="register">Register</h2>
|
||
<p>Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions.</p>
|
||
<p>Product: 150–250 ms on most transitions. Motion conveys state — feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it.</p>
|
||
<hr>
|
||
<h2 id="assess-animation-opportunities">Assess Animation Opportunities</h2>
|
||
<p>Analyze where motion would improve the experience:</p>
|
||
<ol>
|
||
<li><p><strong>Identify static areas</strong>:</p>
|
||
<ul>
|
||
<li><strong>Missing feedback</strong>: Actions without visual acknowledgment (button clicks, form submission, etc.)</li>
|
||
<li><strong>Jarring transitions</strong>: Instant state changes that feel abrupt (show/hide, page loads, route changes)</li>
|
||
<li><strong>Unclear relationships</strong>: Spatial or hierarchical relationships that aren't obvious</li>
|
||
<li><strong>Lack of delight</strong>: Functional but joyless interactions</li>
|
||
<li><strong>Missed guidance</strong>: Opportunities to direct attention or explain behavior</li>
|
||
</ul>
|
||
</li>
|
||
<li><p><strong>Understand the context</strong>:</p>
|
||
<ul>
|
||
<li>What's the personality? (Playful vs serious, energetic vs calm)</li>
|
||
<li>What's the performance budget? (Mobile-first? Complex page?)</li>
|
||
<li>Who's the audience? (Motion-sensitive users? Power users who want speed?)</li>
|
||
<li>What matters most? (One hero animation vs many micro-interactions?)</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
<p>If any of these are unclear from the codebase, STOP and call the AskUserQuestion tool to clarify.</p>
|
||
<p><strong>CRITICAL</strong>: Respect <code>prefers-reduced-motion</code>. Always provide non-animated alternatives for users who need them.</p>
|
||
<h2 id="plan-animation-strategy">Plan Animation Strategy</h2>
|
||
<p>Create a purposeful animation plan:</p>
|
||
<ul>
|
||
<li><strong>Hero moment</strong>: What's the ONE signature animation? (Page load? Hero section? Key interaction?)</li>
|
||
<li><strong>Feedback layer</strong>: Which interactions need acknowledgment?</li>
|
||
<li><strong>Transition layer</strong>: Which state changes need smoothing?</li>
|
||
<li><strong>Delight layer</strong>: Where can we surprise and delight?</li>
|
||
</ul>
|
||
<p><strong>IMPORTANT</strong>: One well-orchestrated experience beats scattered animations everywhere. Focus on high-impact moments.</p>
|
||
<h2 id="implement-animations">Implement Animations</h2>
|
||
<p>Add motion systematically across these categories:</p>
|
||
<h3 id="entrance-animations">Entrance Animations</h3>
|
||
<ul>
|
||
<li><strong>Page load choreography</strong>: Stagger element reveals (100-150ms delays), fade + slide combinations</li>
|
||
<li><strong>Hero section</strong>: Dramatic entrance for primary content (scale, parallax, or creative effects)</li>
|
||
<li><strong>Content reveals</strong>: Scroll-triggered animations using intersection observer</li>
|
||
<li><strong>Modal/drawer entry</strong>: Smooth slide + fade, backdrop fade, focus management</li>
|
||
</ul>
|
||
<h3 id="micro-interactions">Micro-interactions</h3>
|
||
<ul>
|
||
<li><strong>Button feedback</strong>:<ul>
|
||
<li>Hover: Subtle scale (1.02-1.05), color shift, shadow increase</li>
|
||
<li>Click: Quick scale down then up (0.95 → 1), ripple effect</li>
|
||
<li>Loading: Spinner or pulse state</li>
|
||
</ul>
|
||
</li>
|
||
<li><strong>Form interactions</strong>:<ul>
|
||
<li>Input focus: Border color transition, slight scale or glow</li>
|
||
<li>Validation: Shake on error, check mark on success, smooth color transitions</li>
|
||
</ul>
|
||
</li>
|
||
<li><strong>Toggle switches</strong>: Smooth slide + color transition (200-300ms)</li>
|
||
<li><strong>Checkboxes/radio</strong>: Check mark animation, ripple effect</li>
|
||
<li><strong>Like/favorite</strong>: Scale + rotation, particle effects, color transition</li>
|
||
</ul>
|
||
<h3 id="state-transitions">State Transitions</h3>
|
||
<ul>
|
||
<li><strong>Show/hide</strong>: Fade + slide (not instant), appropriate timing (200-300ms)</li>
|
||
<li><strong>Expand/collapse</strong>: Height transition with overflow handling, icon rotation</li>
|
||
<li><strong>Loading states</strong>: Skeleton screen fades, spinner animations, progress bars</li>
|
||
<li><strong>Success/error</strong>: Color transitions, icon animations, gentle scale pulse</li>
|
||
<li><strong>Enable/disable</strong>: Opacity transitions, cursor changes</li>
|
||
</ul>
|
||
<h3 id="navigation-flow">Navigation & Flow</h3>
|
||
<ul>
|
||
<li><strong>Page transitions</strong>: Crossfade between routes, shared element transitions</li>
|
||
<li><strong>Tab switching</strong>: Slide indicator, content fade/slide</li>
|
||
<li><strong>Carousel/slider</strong>: Smooth transforms, snap points, momentum</li>
|
||
<li><strong>Scroll effects</strong>: Parallax layers, sticky headers with state changes, scroll progress indicators</li>
|
||
</ul>
|
||
<h3 id="feedback-guidance">Feedback & Guidance</h3>
|
||
<ul>
|
||
<li><strong>Hover hints</strong>: Tooltip fade-ins, cursor changes, element highlights</li>
|
||
<li><strong>Drag & drop</strong>: Lift effect (shadow + scale), drop zone highlights, smooth repositioning</li>
|
||
<li><strong>Copy/paste</strong>: Brief highlight flash on paste, "copied" confirmation</li>
|
||
<li><strong>Focus flow</strong>: Highlight path through form or workflow</li>
|
||
</ul>
|
||
<h3 id="delight-moments">Delight Moments</h3>
|
||
<ul>
|
||
<li><strong>Empty states</strong>: Subtle floating animations on illustrations</li>
|
||
<li><strong>Completed actions</strong>: Confetti, check mark flourish, success celebrations</li>
|
||
<li><strong>Easter eggs</strong>: Hidden interactions for discovery</li>
|
||
<li><strong>Contextual animation</strong>: Weather effects, time-of-day themes, seasonal touches</li>
|
||
</ul>
|
||
<h2 id="technical-implementation">Technical Implementation</h2>
|
||
<p>Use appropriate techniques for each animation:</p>
|
||
<h3 id="timing-easing">Timing & Easing</h3>
|
||
<p><strong>Durations by purpose:</strong></p>
|
||
<ul>
|
||
<li><strong>100-150ms</strong>: Instant feedback (button press, toggle)</li>
|
||
<li><strong>200-300ms</strong>: State changes (hover, menu open)</li>
|
||
<li><strong>300-500ms</strong>: Layout changes (accordion, modal)</li>
|
||
<li><strong>500-800ms</strong>: Entrance animations (page load)</li>
|
||
</ul>
|
||
<p><strong>Easing curves (use these, not CSS defaults):</strong></p>
|
||
<div class="code-block-wrap"><pre class="code-block code-block--css"><code>/* Recommended - natural deceleration */
|
||
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); /* Smooth, refined */
|
||
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); /* Slightly snappier */
|
||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); /* Confident, decisive */
|
||
|
||
/* AVOID - feel dated and tacky */
|
||
/* bounce: cubic-bezier(0.34, 1.56, 0.64, 1); */
|
||
/* elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6); */</code></pre><button class="code-block-copy" type="button" data-copy="/* Recommended - natural deceleration */
|
||
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); /* Smooth, refined */
|
||
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); /* Slightly snappier */
|
||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); /* Confident, decisive */
|
||
|
||
/* AVOID - feel dated and tacky */
|
||
/* bounce: cubic-bezier(0.34, 1.56, 0.64, 1); */
|
||
/* elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6); */" aria-label="Copy to clipboard"></button></div>
|
||
<p><strong>Exit animations are faster than entrances.</strong> Use ~75% of enter duration.</p>
|
||
<h3 id="css-animations">CSS Animations</h3>
|
||
<div class="code-block-wrap"><pre class="code-block code-block--css"><code>/* Prefer for simple, declarative animations */
|
||
- transitions for state changes
|
||
- @keyframes for complex sequences
|
||
- transform and opacity for reliable movement
|
||
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth</code></pre><button class="code-block-copy" type="button" data-copy="/* Prefer for simple, declarative animations */
|
||
- transitions for state changes
|
||
- @keyframes for complex sequences
|
||
- transform and opacity for reliable movement
|
||
- blur, filters, masks, clip paths, shadows, and color shifts for premium atmospheric effects when verified smooth" aria-label="Copy to clipboard"></button></div>
|
||
<h3 id="javascript-animation">JavaScript Animation</h3>
|
||
<div class="code-block-wrap"><pre class="code-block code-block--javascript"><code>/* Use for complex, interactive animations */
|
||
- Web Animations API for programmatic control
|
||
- Framer Motion for React
|
||
- GSAP for complex sequences</code></pre><button class="code-block-copy" type="button" data-copy="/* Use for complex, interactive animations */
|
||
- Web Animations API for programmatic control
|
||
- Framer Motion for React
|
||
- GSAP for complex sequences" aria-label="Copy to clipboard"></button></div>
|
||
<h3 id="performance">Performance</h3>
|
||
<ul>
|
||
<li><strong>Motion materials</strong>: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect</li>
|
||
<li><strong>Layout safety</strong>: Avoid casual animation of layout-driving properties (<code>width</code>, <code>height</code>, <code>top</code>, <code>left</code>, margins)</li>
|
||
<li><strong>will-change</strong>: Add sparingly for known expensive animations</li>
|
||
<li><strong>Bound expensive effects</strong>: Keep blur/filter/shadow areas small or isolated, use <code>contain</code> where appropriate</li>
|
||
<li><strong>Monitor FPS</strong>: Ensure 60fps on target devices</li>
|
||
</ul>
|
||
<h3 id="accessibility">Accessibility</h3>
|
||
<div class="code-block-wrap"><pre class="code-block code-block--css"><code>@media (prefers-reduced-motion: reduce) {
|
||
* {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
}
|
||
}</code></pre><button class="code-block-copy" type="button" data-copy="@media (prefers-reduced-motion: reduce) {
|
||
* {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
}
|
||
}" aria-label="Copy to clipboard"></button></div>
|
||
<p><strong>NEVER</strong>:</p>
|
||
<ul>
|
||
<li>Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself</li>
|
||
<li>Animate layout properties casually (<code>width</code>, <code>height</code>, <code>top</code>, <code>left</code>, margins) when transform, FLIP, or grid-based techniques would work</li>
|
||
<li>Use durations over 500ms for feedback—it feels laggy</li>
|
||
<li>Animate without purpose—every animation needs a reason</li>
|
||
<li>Ignore <code>prefers-reduced-motion</code>—this is an accessibility violation</li>
|
||
<li>Animate everything—animation fatigue makes interfaces feel exhausting</li>
|
||
<li>Block interaction during animations unless intentional</li>
|
||
</ul>
|
||
<h2 id="verify-quality">Verify Quality</h2>
|
||
<p>Test animations thoroughly:</p>
|
||
<ul>
|
||
<li><strong>Smooth at 60fps</strong>: No jank on target devices</li>
|
||
<li><strong>Feels natural</strong>: Easing curves feel organic, not robotic</li>
|
||
<li><strong>Appropriate timing</strong>: Not too fast (jarring) or too slow (laggy)</li>
|
||
<li><strong>Reduced motion works</strong>: Animations disabled or simplified appropriately</li>
|
||
<li><strong>Doesn't block</strong>: Users can interact during/after animations</li>
|
||
<li><strong>Adds value</strong>: Makes interface clearer or more delightful</li>
|
||
</ul>
|
||
<p>Remember: Motion should enhance understanding and provide feedback, not just add decoration. Animate with purpose, respect performance constraints, and always consider accessibility. Great animation is invisible - it just makes everything feel right.</p>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
|
||
</article>
|
||
|
||
</div>
|
||
</div>
|
||
</main>
|
||
<script>
|
||
// Copy buttons on rendered code blocks
|
||
document.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('[data-copy]');
|
||
if (!btn) return;
|
||
const text = btn.getAttribute('data-copy');
|
||
if (!text) return;
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
btn.classList.add('is-copied');
|
||
setTimeout(() => btn.classList.remove('is-copied'), 1500);
|
||
}).catch(() => {});
|
||
});
|
||
|
||
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
|
||
document.addEventListener('click', (e) => {
|
||
const toggle = e.target.closest('.skills-sidebar-toggle');
|
||
if (!toggle) return;
|
||
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
||
toggle.setAttribute('aria-expanded', String(!expanded));
|
||
});
|
||
|
||
// Before/after split-compare: drag on touch, hover OR drag on mouse.
|
||
// Pointer events attach to the padded .split-comparison wrapper so
|
||
// there is a ~20px invisible buffer around the visible box. The
|
||
// divider only snaps back when the pointer leaves that outer buffer.
|
||
(function initSplitCompare() {
|
||
const wrappers = document.querySelectorAll('.split-comparison');
|
||
if (wrappers.length === 0) return;
|
||
const hasHover = matchMedia('(hover: hover)').matches;
|
||
const DEFAULT_POSITION = 50;
|
||
|
||
for (const wrapper of wrappers) {
|
||
const container = wrapper.querySelector('.split-container');
|
||
const splitAfter = wrapper.querySelector('.split-after');
|
||
const splitDivider = wrapper.querySelector('.split-divider');
|
||
if (!container || !splitAfter || !splitDivider) continue;
|
||
|
||
const tanAngle = Math.tan(10 * Math.PI / 180);
|
||
let skewOffset = 8;
|
||
const recalcSkew = () => {
|
||
const r = container.getBoundingClientRect();
|
||
if (r.width > 0 && r.height > 0) {
|
||
skewOffset = 50 * r.height * tanAngle / r.width;
|
||
}
|
||
};
|
||
recalcSkew();
|
||
window.addEventListener('resize', recalcSkew, { passive: true });
|
||
|
||
let targetX = DEFAULT_POSITION;
|
||
let currentX = DEFAULT_POSITION;
|
||
let rafId = null;
|
||
|
||
const paint = (pct) => {
|
||
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
|
||
splitAfter.style.clipPath =
|
||
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
|
||
splitDivider.style.left = `${x}%`;
|
||
};
|
||
|
||
const step = () => {
|
||
currentX += (targetX - currentX) * 0.2;
|
||
if (Math.abs(targetX - currentX) < 0.1) {
|
||
currentX = targetX;
|
||
rafId = null;
|
||
} else {
|
||
rafId = requestAnimationFrame(step);
|
||
}
|
||
paint(currentX);
|
||
};
|
||
|
||
const setTarget = (pct) => {
|
||
targetX = pct;
|
||
if (rafId === null) rafId = requestAnimationFrame(step);
|
||
};
|
||
|
||
paint(DEFAULT_POSITION);
|
||
|
||
// Percentage is always relative to the VISIBLE .split-container,
|
||
// not the padded .split-comparison wrapper. The pointer event
|
||
// target is the wrapper but the clip-path math uses the inner box.
|
||
const pctFromClientX = (clientX) => {
|
||
const rect = container.getBoundingClientRect();
|
||
return ((clientX - rect.left) / rect.width) * 100;
|
||
};
|
||
|
||
let hovering = false;
|
||
let dragging = false;
|
||
|
||
wrapper.addEventListener('pointerenter', (e) => {
|
||
if (hasHover && e.pointerType === 'mouse') {
|
||
hovering = true;
|
||
}
|
||
});
|
||
|
||
wrapper.addEventListener('pointerdown', (e) => {
|
||
dragging = true;
|
||
wrapper.setPointerCapture(e.pointerId);
|
||
setTarget(pctFromClientX(e.clientX));
|
||
});
|
||
|
||
wrapper.addEventListener('pointermove', (e) => {
|
||
if (dragging || hovering) {
|
||
setTarget(pctFromClientX(e.clientX));
|
||
}
|
||
});
|
||
|
||
const endDrag = (e) => {
|
||
if (dragging) {
|
||
dragging = false;
|
||
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
|
||
}
|
||
};
|
||
|
||
wrapper.addEventListener('pointerup', endDrag);
|
||
wrapper.addEventListener('pointercancel', endDrag);
|
||
|
||
wrapper.addEventListener('pointerleave', (e) => {
|
||
endDrag(e);
|
||
if (hovering) {
|
||
hovering = false;
|
||
setTarget(DEFAULT_POSITION);
|
||
}
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|