Ensures engaging user experience through validation of animations, transitions, micro-interactions, and feedback states, preventing flat/static interfaces that lack polish and engagement. Works with Tanstack Start (React) + shadcn/ui components.
Validates animations, transitions, and micro-interactions for interactive elements. Automatically activates when creating buttons, links, forms, or async operations to ensure engaging user feedback.
/plugin marketplace add hirefrank/hirefrank-marketplace/plugin install edge-stack@hirefrank-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This SKILL automatically activates when:
// These patterns trigger alerts:
// No hover state
<Button onClick={submit}>Submit</Button>
// No loading state during async action
<Button onClick={async () => await submitForm()}>Save</Button>
// Jarring state change (no transition)
{showContent && <div>Content</div>}
// No focus state
<a href="/page" className="text-blue-500">Link</a>
// Form without feedback
<form onSubmit={handleSubmit}>
<Input value={value} onChange={setValue} />
<button type="submit">Submit</button>
</form>
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Send } from "lucide-react"
import { cn } from "@/lib/utils"
// These patterns are validated as correct:
// Hover state with smooth transition
<Button
className="transition-all duration-300 hover:scale-105 hover:shadow-xl active:scale-95"
onClick={submit}
>
Submit
</Button>
// Loading state with visual feedback
<Button
disabled={isSubmitting}
className="transition-all duration-200 group"
onClick={handleSubmit}
>
<span className="flex items-center gap-2">
{!isSubmitting && (
<Send className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
)}
{isSubmitting ? 'Submitting...' : 'Submit'}
</span>
</Button>
// Smooth state transition (using framer-motion or CSS)
<div
className={cn(
"transition-all duration-300 ease-out",
showContent ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
)}
>
{showContent && <div>Content</div>}
</div>
// Focus state with ring
<a
href="/page"
className="text-blue-500 transition-colors duration-200 hover:text-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
>
Link
</a>
<!-- Form with success/error feedback -->
<form onSubmit={(e) => { e.preventDefault(); handleSubmit" className="space-y-4">
<Input
value="value"
error={errors.value"
className="transition-all duration-200"
/>
<Button
type="submit"
loading={isSubmitting"
disabled={isSubmitting"
className="transition-all duration-300 hover:scale-105"
>
Submit
</Button>
<!-- Success message with animation -->
<Transition name="fade">
<Alert
if="showSuccess"
color="green"
icon="i-heroicons-check-circle"
title="Success!"
className="animate-in slide-in-from-top"
/>
</Transition>
</form>
frontend-design-specialist agenttanstack-ui-architect agentedge-performance-oracle agentaccessibility-guardian agent<!-- ❌ Critical: No hover feedback -->
<Button onClick="handleClick">
Click me
</Button>
<!-- ✅ Correct: Multi-dimensional hover effects -->
<Button
className="
transition-all duration-300 ease-out
hover:scale-105 hover:shadow-xl hover:-rotate-1
active:scale-95 active:rotate-0
focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary-500
"
onClick="handleClick"
>
<span className="inline-flex items-center gap-2">
Click me
<Icon
name="i-heroicons-arrow-right"
className="transition-transform duration-300 group-hover:translate-x-1"
/>
</span>
</Button>
<!-- ❌ Critical: No loading feedback during async action -->
const submitForm = async () => {
await api.submit(formData);
};
<Button onClick="submitForm">
Submit
</Button>
<!-- ✅ Correct: Complete loading state with animations -->
const isSubmitting = ref(false);
const showSuccess = ref(false);
const submitForm = async () => {
isSubmitting.value = true;
try {
await api.submit(formData);
showSuccess.value = true;
setTimeout(() => showSuccess.value = false, 3000);
} catch (error) {
// Error handling
} finally {
isSubmitting.value = false;
}
};
<div className="space-y-4">
<Button
loading={isSubmitting"
disabled={isSubmitting"
className="
transition-all duration-300
hover:scale-105 hover:shadow-xl
disabled:opacity-50 disabled:cursor-not-allowed
"
onClick="submitForm"
>
<span className="flex items-center gap-2">
<Icon
if="!isSubmitting"
name="i-heroicons-paper-airplane"
className="transition-all duration-300 group-hover:translate-x-1 group-hover:-translate-y-1"
/>
{ isSubmitting ? 'Submitting...' : 'Submit'}
</span>
</Button>
<!-- Success feedback with animation -->
<Transition
enter-active-className="transition-all duration-500 ease-out"
enter-from-className="opacity-0 scale-50"
enter-to-className="opacity-100 scale-100"
leave-active-className="transition-all duration-300 ease-in"
leave-from-className="opacity-100 scale-100"
leave-to-className="opacity-0 scale-50"
>
<Alert
if="showSuccess"
color="green"
icon="i-heroicons-check-circle"
title="Success!"
description="Your form has been submitted."
/>
</Transition>
</div>
<!-- ❌ Critical: Content appears/disappears abruptly -->
<div>
<Button onClick="showContent = !showContent">
Toggle
</Button>
<div if="showContent">
<p>This content appears instantly (jarring)</p>
</div>
</div>
<!-- ✅ Correct: Smooth transitions -->
<div className="space-y-4">
<Button
className="transition-all duration-300 hover:scale-105"
onClick="showContent = !showContent"
>
{ showContent ? 'Hide' : 'Show'} Content
</Button>
<Transition
enter-active-className="transition-all duration-300 ease-out"
enter-from-className="opacity-0 translate-y-4 scale-95"
enter-to-className="opacity-100 translate-y-0 scale-100"
leave-active-className="transition-all duration-200 ease-in"
leave-from-className="opacity-100 translate-y-0 scale-100"
leave-to-className="opacity-0 translate-y-4 scale-95"
>
<div if="showContent" className="p-6 bg-gray-50 dark:bg-gray-800 rounded-lg">
<p>This content transitions smoothly</p>
</div>
</Transition>
</div>
<!-- ❌ Critical: No visible focus state -->
<nav>
<a href="/" className="text-gray-700">Home</a>
<a href="/about" className="text-gray-700">About</a>
<a href="/contact" className="text-gray-700">Contact</a>
</nav>
<!-- ✅ Correct: Clear focus states for keyboard navigation -->
<nav className="flex gap-4">
<a
href="/"
className="
text-gray-700 dark:text-gray-300
transition-all duration-200
hover:text-primary-600 hover:translate-y-[-2px]
focus:outline-none
focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2
rounded px-3 py-2
"
>
Home
</a>
<a
href="/about"
className="
text-gray-700 dark:text-gray-300
transition-all duration-200
hover:text-primary-600 hover:translate-y-[-2px]
focus:outline-none
focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2
rounded px-3 py-2
"
>
About
</a>
<a
href="/contact"
className="
text-gray-700 dark:text-gray-300
transition-all duration-200
hover:text-primary-600 hover:translate-y-[-2px]
focus:outline-none
focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2
rounded px-3 py-2
"
>
Contact
</a>
</nav>
<!-- ❌ P2: Static icons without micro-interactions -->
<Button icon="i-heroicons-heart">
Like
</Button>
<!-- ✅ Correct: Animated icon micro-interaction -->
const isLiked = ref(false);
const heartScale = ref(1);
const toggleLike = () => {
isLiked.value = !isLiked.value;
// Bounce animation
heartScale.value = 1.3;
setTimeout(() => heartScale.value = 1, 200);
};
<Button
:color="isLiked ? 'red' : 'gray'"
className="transition-all duration-300 hover:scale-105"
onClick="toggleLike"
>
<span className="inline-flex items-center gap-2">
<Icon
:name="isLiked ? 'i-heroicons-heart-solid' : 'i-heroicons-heart'"
:style="{ transform: `scale(${heartScale})` }"
:className="[
'transition-all duration-200',
isLiked ? 'text-red-500 animate-pulse' : 'text-gray-500'
]"
/>
{ isLiked ? 'Liked' : 'Like'}
</span>
</Button>
✅ Performant Properties (GPU-accelerated):
transform (translate, scale, rotate)opacityfilter (backdrop-blur, etc.)❌ Avoid Animating (causes reflow/repaint):
width, heighttop, left, right, bottommargin, paddingborder-width<!-- ❌ P2: Animating width (causes reflow) -->
<div className="transition-all hover:w-64">Content</div>
<!-- ✅ Correct: Using transform (GPU-accelerated) -->
<div className="transition-transform hover:scale-110">Content</div>
<!-- Context-appropriate durations -->
<Button className="transition-all duration-200 hover:scale-105">
<!-- Fast hover: 200ms -->
</Button>
<Transition
enter-active-className="transition-all duration-300"
leave-active-className="transition-all duration-300"
>
<!-- Content change: 300ms -->
<div if="show">Content</div>
</Transition>
<div className="animate-in slide-in-from-bottom duration-500">
<!-- Page load: 500ms -->
Main content
</div>
ease-out: Starting animations (entering content)ease-in: Ending animations (exiting content)ease-in-out: Bidirectional animationslinear: Loading spinners, continuous animations<!-- Appropriate easing -->
<Transition
enter-active-className="transition-all duration-300 ease-out"
leave-active-className="transition-all duration-200 ease-in"
>
<div if="show">Content</div>
</Transition>
const items = ref([1, 2, 3, 4, 5]);
<TransitionGroup
name="list"
tag="div"
className="space-y-2"
>
<div
map((item, index) in items"
:key="item"
:style="{ transitionDelay: `${index * 50}ms` }"
className="
transition-all duration-300 ease-out
hover:scale-105 hover:shadow-lg
"
>
Item { item}
</div>
</TransitionGroup>
<style scoped>
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
.list-move {
transition: transform 0.3s ease;
}
</style>
const showSuccess = ref(false);
const celebrate = () => {
showSuccess.value = true;
// Confetti or celebration animation here
setTimeout(() => showSuccess.value = false, 3000);
};
<div>
<Button
onClick="celebrate"
className="transition-all duration-300 hover:scale-110 hover:rotate-3"
>
Complete Task
</Button>
<Transition
enter-active-className="transition-all duration-500 ease-out"
enter-from-className="opacity-0 scale-0 rotate-180"
enter-to-className="opacity-100 scale-100 rotate-0"
>
<div
if="showSuccess"
className="fixed inset-0 flex items-center justify-center bg-black/20 backdrop-blur-sm"
>
<div className="bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-2xl">
<Icon
name="i-heroicons-check-circle"
className="w-16 h-16 text-green-500 animate-bounce"
/>
<p className="mt-4 text-xl font-heading">Success!</p>
</div>
</div>
</Transition>
</div>
<div if="loading" className="space-y-4">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2 mt-2"></div>
<div className="h-32 bg-gray-200 dark:bg-gray-700 rounded mt-4"></div>
</div>
</div>
<Transition
enter-active-className="transition-all duration-500 ease-out"
enter-from-className="opacity-0 translate-y-4"
enter-to-className="opacity-100 translate-y-0"
>
<div if="!loading">
<!-- Actual content -->
</div>
</Transition>
While this SKILL doesn't directly use MCP servers, it complements MCP-enhanced agents:
// Developer adds: <Button onClick="submit">Submit</Button>
// SKILL immediately activates: "⚠️ P1: Button lacks hover state. Add transition utilities: class='transition-all duration-300 hover:scale-105'"
// Developer creates: const submitForm = async () => { await api.call(); }
// SKILL immediately activates: "⚠️ P1: Async action without loading state. Add :loading and :disabled props to button."
// Developer adds: <div if="show">Content</div>
// SKILL immediately activates: "⚠️ P1: Content appears abruptly. Wrap with <Transition> for smooth state changes."
// SKILL runs comprehensive check: "✅ Animation validation passed. 45 interactive elements with hover states, 12 async actions with loading feedback, 8 smooth transitions detected."
This SKILL ensures every interactive element provides engaging visual feedback, preventing the flat, static appearance that makes interfaces feel unpolished and reduces user engagement.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.