Comprehensive UI/frontend development skill for React, Next.js, Vue, Svelte, and Tailwind CSS. Provides guidance on component architecture, accessibility, responsive design, state management, and modern CSS techniques. Automatically triggers for any frontend/UI work including building components, pages, styling, and interactive features.
This skill provides comprehensive guidance for modern UI/frontend development across multiple frameworks and libraries. Use this skill whenever you're working on user interfaces, components, styling, or interactive features.
This skill automatically activates when:
IMPORTANT: Before starting ANY new UI work, you should invoke the skill to help the user select a design direction. This ensures consistent, intentional aesthetics from the start.
ui-portfolio-designTo invoke the portfolio selection:
/docs/ui-portfolio/index.htmlSkip portfolio selection only when:
Single Responsibility
Composition Over Complexity
// Good: Composable, reusable
<Card>
<CardHeader>
<CardTitle>Dashboard</CardTitle>
</CardHeader>
<CardContent>
{children}
</CardContent>
</Card>
// Avoid: Monolithic with too many props
<Card
title="Dashboard"
showHeader
headerAlign="left"
contentPadding="large"
// ... 20 more props
/>
Props Design
children over render props when possibleAlways Include
<button>, <nav>, <main>, etc.)aria-label, aria-describedby)Common Patterns
// Accessible button
<button
onClick={handleClick}
aria-label="Close dialog"
aria-pressed={isActive}
>
<Icon aria-hidden="true" />
Close
</button>
// Accessible form input
<div>
<label htmlFor="email" className="block mb-2">
Email Address
</label>
<input
id="email"
type="email"
aria-describedby="email-hint"
aria-invalid={hasError}
/>
<span id="email-hint" className="text-sm text-gray-600">
We'll never share your email
</span>
</div>
// Skip to main content
<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>
<main id="main-content">
{/* content */}
</main>
Mobile-First Approach
sm:, md:, lg:, xl:, 2xl:Fluid Typography & Spacing
/* Use clamp for fluid sizing */
font-size: clamp(1rem, 2vw + 0.5rem, 2rem);
/* Or Tailwind responsive utilities */
<h1 className="text-2xl sm:text-3xl lg:text-5xl">
Container Queries (when supported)
@container (min-width: 400px) {
.card { grid-template-columns: 1fr 1fr; }
}
Local State First
useState, ref) for UI-only stateServer State
Form State
// Good: Server state with TanStack Query
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
// Good: Form state with React Hook Form
const { register, handleSubmit, formState: { errors } } = useForm()
Lazy Loading
// Code splitting
const Dashboard = lazy(() => import('./Dashboard'))
// Image optimization
<Image
src="/hero.jpg"
alt="Hero"
loading="lazy"
width={1200}
height={600}
/>
Memoization
useMemo for expensive calculationsuseCallback for stable function referencesmemo() for expensive componentsVirtual Lists
Tailwind CSS
@apply sparingly (components only)@applyw-[347px]Component Variants
// Using class-variance-authority (cva)
const buttonVariants = cva(
"rounded font-medium transition-colors", // base
{
variants: {
variant: {
default: "bg-blue-600 text-white hover:bg-blue-700",
outline: "border border-gray-300 hover:bg-gray-50",
ghost: "hover:bg-gray-100",
},
size: {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
)
CSS Modules (when not using Tailwind)
Component.module.csscomposesCompound Components
// API that reads well
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
</TabsList>
<TabsContent value="overview">
{/* content */}
</TabsContent>
</Tabs>
Render Props (when composition isn't enough)
<DataFetcher url="/api/users">
{({ data, loading, error }) => (
loading ? <Spinner /> : <UserList users={data} />
)}
</DataFetcher>
Slots Pattern (React, Vue)
// React
<Layout
header={<Header />}
sidebar={<Sidebar />}
footer={<Footer />}
>
{children}
</Layout>
Error Boundaries (React)
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>
Error States in UI
if (error) {
return (
<Alert variant="error">
<AlertTitle>Failed to load data</AlertTitle>
<AlertDescription>
{error.message}
<Button onClick={retry}>Try Again</Button>
</AlertDescription>
</Alert>
)
}
Framer Motion (React)
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
{content}
</motion.div>
CSS Transitions (Tailwind)
<div className="transition-all duration-200 hover:scale-105">
Reduced Motion
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Component Tests
getByRole, getByLabelText)Visual Regression
File Organization
src/
components/
ui/ # Reusable UI primitives (Button, Input, Card)
features/ # Feature-specific components
hooks/ # Custom hooks
lib/ # Utilities, API clients
app/ # Next.js app router
Server Components (Next.js 13+)
Data Fetching
// Server Component
async function Page() {
const data = await fetch('https://api.example.com/data')
return <Dashboard data={data} />
}
// Client Component with TanStack Query
'use client'
function Dashboard() {
const { data } = useQuery(...)
return <div>{data}</div>
}
Composition API
<script setup> syntaxReactivity
// Reactive state
const state = reactive({ count: 0 })
// Computed properties
const doubled = computed(() => state.count * 2)
// Watch for changes
watch(() => state.count, (newVal) => {
console.log(newVal)
})
Reactivity
<script>
let count = 0
$: doubled = count * 2 // Reactive declaration
</script>
<button on:click={() => count++}>
Count: {count}, Doubled: {doubled}
</button>
Stores
// Writable store
const count = writable(0)
// Derived store
const doubled = derived(count, $count => $count * 2)
// Auto-subscription in components
<script>
import { count } from './stores'
</script>
<div>{$count}</div>
Over-Abstraction
// TOO GENERIC
<FlexBox direction="column" justify="center" align="items" gap={4}>
Prop Drilling
Inline Functions in JSX (if causing perf issues)
// Can cause re-renders
<button onClick={() => handleClick(id)}>
Magic Numbers
// Bad
<div className="w-[347px] h-[291px]">
// Good
<div className="w-80 h-72"> // or semantic sizes
Ignoring Loading States
// Bad: Just shows nothing while loading
{data && <UserList users={data} />}
// Good: Show skeleton or spinner
{isLoading ? <Skeleton /> : <UserList users={data} />}
Use Semantic HTML
<button> for actions, not <div onClick>
<a> for navigation, not <div onClick>
<nav>, <main>, <article>, <section>
Keep Props Simple
// Good: Clear, typed props
interface ButtonProps {
variant?: 'default' | 'outline' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
onClick?: () => void
children: React.ReactNode
}
Handle Edge Cases
Essential
UI Components
Animation
Icons
Check for existing design system
components/ui/If no design system exists
ui-portfolio-design skillBuild components
Test & refine
Understand current patterns
Maintain consistency
Improve incrementally
Remember: Before starting new UI work, consider invoking the ui-portfolio-design skill to select a design direction. This ensures intentional, cohesive aesthetics from the start.