Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add ccalebcarter/purria-skills --skill "react-game-ui"
Install specific skill from multi-skill repository
# Description
React game UI patterns using shadcn/ui, Tailwind, and Framer Motion for polished game interfaces. Use when building HUDs, resource bars, scoreboards, modals, tooltips, card components, or any game UI. Includes micro-interactions, animations, responsive layouts, and accessibility for games. Triggers on requests for game interface components, UI animations, or shadcn/ui game patterns.
# SKILL.md
name: react-game-ui
description: React game UI patterns using shadcn/ui, Tailwind, and Framer Motion for polished game interfaces. Use when building HUDs, resource bars, scoreboards, modals, tooltips, card components, or any game UI. Includes micro-interactions, animations, responsive layouts, and accessibility for games. Triggers on requests for game interface components, UI animations, or shadcn/ui game patterns.
React Game UI
Production-ready UI patterns for game interfaces using shadcn/ui, Tailwind, and Framer Motion.
Core Components
Resource Bar
import { cn } from '@/lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
interface ResourceBarProps {
icon: React.ReactNode;
value: number;
maxValue?: number;
label: string;
color?: 'gold' | 'green' | 'blue' | 'red';
}
const colorMap = {
gold: 'from-amber-400 to-yellow-500',
green: 'from-emerald-400 to-green-500',
blue: 'from-sky-400 to-blue-500',
red: 'from-rose-400 to-red-500',
};
export function ResourceBar({
icon,
value,
maxValue,
label,
color = 'gold'
}: ResourceBarProps) {
return (
<div className="flex items-center gap-2 bg-black/40 backdrop-blur-sm rounded-lg px-3 py-2">
<div className={cn(
'w-8 h-8 rounded-full flex items-center justify-center',
`bg-gradient-to-br ${colorMap[color]}`
)}>
{icon}
</div>
<div className="flex flex-col">
<span className="text-xs text-white/60 uppercase tracking-wide">{label}</span>
<motion.span
key={value}
initial={{ scale: 1.2, color: '#ffd700' }}
animate={{ scale: 1, color: '#ffffff' }}
className="text-lg font-bold text-white tabular-nums"
>
{value.toLocaleString()}
{maxValue && <span className="text-white/40">/{maxValue}</span>}
</motion.span>
</div>
</div>
);
}
Animated Counter
import { useEffect, useRef } from 'react';
import { motion, useSpring, useTransform } from 'framer-motion';
interface AnimatedCounterProps {
value: number;
duration?: number;
className?: string;
}
export function AnimatedCounter({ value, duration = 0.5, className }: AnimatedCounterProps) {
const spring = useSpring(0, { duration: duration * 1000 });
const display = useTransform(spring, (v) => Math.floor(v).toLocaleString());
useEffect(() => {
spring.set(value);
}, [spring, value]);
return <motion.span className={className}>{display}</motion.span>;
}
Meter/Progress Component
interface MeterProps {
value: number; // 0-100
label: string;
tier?: 'mini' | 'major' | 'grand';
showValue?: boolean;
}
const tierStyles = {
mini: 'h-2',
major: 'h-3',
grand: 'h-4',
};
export function Meter({ value, label, tier = 'mini', showValue = true }: MeterProps) {
const clampedValue = Math.max(0, Math.min(100, value));
return (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-white/80">{label}</span>
{showValue && (
<span className="text-white/60 tabular-nums">{clampedValue}%</span>
)}
</div>
<div className={cn(
'w-full bg-white/10 rounded-full overflow-hidden',
tierStyles[tier]
)}>
<motion.div
className={cn(
'h-full rounded-full',
clampedValue >= 50
? 'bg-gradient-to-r from-emerald-500 to-green-400'
: 'bg-gradient-to-r from-amber-500 to-orange-400'
)}
initial={{ width: 0 }}
animate={{ width: `${clampedValue}%` }}
transition={{ type: 'spring', stiffness: 100, damping: 15 }}
/>
</div>
</div>
);
}
Game Card Component
import { motion } from 'framer-motion';
interface GameCardProps {
suit: 'hearts' | 'diamonds' | 'clubs' | 'spades';
rank: string;
faceDown?: boolean;
onClick?: () => void;
}
export function GameCard({ suit, rank, faceDown = false, onClick }: GameCardProps) {
const isRed = suit === 'hearts' || suit === 'diamonds';
return (
<motion.div
className="relative w-[90px] h-[126px] cursor-pointer perspective-1000"
onClick={onClick}
whileHover={{ y: -8, transition: { duration: 0.2 } }}
whileTap={{ scale: 0.95 }}
>
<motion.div
className="w-full h-full relative"
style={{ transformStyle: 'preserve-3d' }}
animate={{ rotateY: faceDown ? 180 : 0 }}
transition={{ duration: 0.6, type: 'spring' }}
>
{/* Front */}
<div
className={cn(
'absolute inset-0 rounded-lg shadow-lg backface-hidden',
'bg-white border-2 border-gray-200',
'flex flex-col items-center justify-center'
)}
>
<span className={cn(
'text-2xl font-bold',
isRed ? 'text-red-600' : 'text-gray-900'
)}>
{rank}
</span>
<span className="text-3xl">{suitSymbol(suit)}</span>
</div>
{/* Back */}
<div
className="absolute inset-0 rounded-lg shadow-lg backface-hidden bg-gradient-to-br from-indigo-600 to-purple-700"
style={{ transform: 'rotateY(180deg)' }}
>
<div className="w-full h-full rounded-lg border-4 border-white/20 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-white/10" />
</div>
</div>
</motion.div>
</motion.div>
);
}
function suitSymbol(suit: string) {
const symbols = { hearts: 'โฅ', diamonds: 'โฆ', clubs: 'โฃ', spades: 'โ ' };
return symbols[suit] || '';
}
Micro-Interactions
Button with Feedback
import { Button } from '@/components/ui/button';
import { motion } from 'framer-motion';
export function GameButton({
children,
onClick,
variant = 'default',
...props
}: React.ComponentProps<typeof Button>) {
return (
<Button
asChild
variant={variant}
onClick={onClick}
{...props}
>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
{children}
</motion.button>
</Button>
);
}
Coin Pop Animation
import { motion, AnimatePresence } from 'framer-motion';
interface CoinPopProps {
amount: number;
position: { x: number; y: number };
onComplete: () => void;
}
export function CoinPop({ amount, position, onComplete }: CoinPopProps) {
return (
<motion.div
className="fixed pointer-events-none z-50 text-2xl font-bold text-yellow-400"
style={{ left: position.x, top: position.y }}
initial={{ opacity: 1, y: 0, scale: 0.5 }}
animate={{ opacity: 0, y: -60, scale: 1.2 }}
exit={{ opacity: 0 }}
transition={{ duration: 1, ease: 'easeOut' }}
onAnimationComplete={onComplete}
>
+{amount} ๐ช
</motion.div>
);
}
Shake on Error
const shakeAnimation = {
x: [0, -10, 10, -10, 10, 0],
transition: { duration: 0.4 }
};
export function ShakeOnError({ error, children }) {
return (
<motion.div animate={error ? shakeAnimation : {}}>
{children}
</motion.div>
);
}
Layout Patterns
Game HUD Layout
export function GameLayout({ children }: { children: React.ReactNode }) {
return (
<div className="relative w-full h-screen overflow-hidden bg-gradient-to-br from-stone-900 via-stone-950 to-black">
{/* Top Bar */}
<header className="absolute top-0 left-0 right-0 z-20 p-4 flex justify-between items-start">
<ResourceBar icon={<Flower />} value={resources.tulipBulbs} label="Tulips" color="green" />
<Scoreboard day={time.day} score={score} />
</header>
{/* Main Content */}
<main className="absolute inset-0 flex items-center justify-center pt-20 pb-20">
{children}
</main>
{/* Bottom Bar */}
<footer className="absolute bottom-0 left-0 right-0 z-20 p-4 flex justify-center">
<PhaseControls />
</footer>
{/* Side Panel */}
<aside className="absolute top-20 right-0 bottom-20 z-10 w-80 p-4">
<MetaPotPanel />
</aside>
</div>
);
}
Responsive Game Board
export function GameBoard({ children }: { children: React.ReactNode }) {
return (
<div className="w-full h-full flex items-center justify-center p-4">
<div className={cn(
'relative',
'w-full max-w-[min(90vw,90vh)]',
'aspect-square'
)}>
{children}
</div>
</div>
);
}
Modal & Dialog Patterns
Game Modal with shadcn
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { motion, AnimatePresence } from 'framer-motion';
export function GameModal({ open, onOpenChange, title, children }) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-gradient-to-br from-stone-800 to-stone-900 border-amber-500/30">
<DialogHeader>
<DialogTitle className="text-amber-400 text-xl">{title}</DialogTitle>
</DialogHeader>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
>
{children}
</motion.div>
</DialogContent>
</Dialog>
);
}
Tooltip for Game Elements
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
export function GameTooltip({ children, content }) {
return (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
className="bg-stone-900 border-amber-500/30 text-white"
sideOffset={8}
>
{content}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
Accessibility for Games
Skip Animation Preference
import { useReducedMotion } from 'framer-motion';
export function AnimatedElement({ children }) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
animate={{ opacity: 1, y: 0 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.3 }}
>
{children}
</motion.div>
);
}
Screen Reader Announcements
import { useEffect, useRef } from 'react';
export function useAnnounce() {
const ref = useRef<HTMLDivElement>(null);
const announce = (message: string) => {
if (ref.current) {
ref.current.textContent = message;
}
};
return { announce, AnnouncerRegion: () => (
<div
ref={ref}
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
/>
)};
}
// Usage
const { announce, AnnouncerRegion } = useAnnounce();
announce('You won 500 coins!');
Performance Tips
- Memoize heavy components:
React.memo()for hex cells, cards - Use CSS transforms: GPU-accelerated vs layout-triggering properties
- Virtualize large lists: Only render visible items
- Debounce rapid updates: Score counters, resource bars
- Lazy load modals: Don't mount until needed
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.