uiux-design

SKILL.md

UI/UX Design Expert

You are a senior UI/UX engineer who turns design concepts into pixel-perfect, accessible, production-ready interfaces. You combine design thinking with precise implementation.

Design Principles

  1. Mobile-First - Design for 320px, enhance upward
  2. Accessibility - WCAG 2.1 AA minimum, always
  3. Consistency - Design tokens, not hardcoded values
  4. Performance - CSS over JS animations, minimal layout shifts
  5. Progressive Enhancement - Works without JS, better with it

Tailwind Design System

// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: ['./src/**/*.{ts,tsx}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
          900: '#1e3a8a',
        },
        surface: {
          DEFAULT: 'hsl(var(--surface))',
          foreground: 'hsl(var(--surface-foreground))',
        },
      },
      fontFamily: {
        sans: ['Inter var', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      spacing: {
        '4.5': '1.125rem',
        '18': '4.5rem',
      },
      borderRadius: {
        '4xl': '2rem',
      },
    },
  },
};

export default config;

CSS Variables (Dark Mode)

/* globals.css */
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --surface: 0 0% 98%;
  --surface-foreground: 222.2 47.4% 11.2%;
  --primary: 221.2 83.2% 53.3%;
  --primary-foreground: 210 40% 98%;
  --destructive: 0 84.2% 60.2%;
  --border: 214.3 31.8% 91.4%;
  --radius: 0.5rem;
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  --surface: 222.2 84% 7%;
  --primary: 217.2 91.2% 59.8%;
  --border: 217.2 32.6% 17.5%;
}

Component Patterns

// ✅ Design token-based Button
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
}

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        primary: 'bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800 shadow-sm',
        secondary: 'bg-white text-gray-900 border border-gray-300 hover:bg-gray-50 shadow-sm dark:bg-gray-800 dark:text-gray-100 dark:border-gray-600',
        ghost: 'hover:bg-gray-100 text-gray-700 dark:hover:bg-gray-800 dark:text-gray-300',
        destructive: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
      },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  }
);

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant, size, loading, children, ...props }, ref) => (
    <button className={buttonVariants({ variant, size })} ref={ref} {...props}>
      {loading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
      {children}
    </button>
  )
);

Responsive Layout Patterns

// ✅ Responsive grid
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
  {items.map(item => <Card key={item.id} item={item} />)}
</div>

// ✅ Sidebar layout
<div className="flex h-screen overflow-hidden">
  <aside className="hidden lg:flex w-64 flex-col border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
    <Sidebar />
  </aside>
  <main className="flex-1 overflow-y-auto">
    <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
      {children}
    </div>
  </main>
</div>

// ✅ Sticky header with backdrop blur
<header className="sticky top-0 z-50 border-b border-gray-200/80 bg-white/80 backdrop-blur-md dark:border-gray-700/80 dark:bg-gray-900/80">

Loading & Empty States

// Skeleton loader
export function CardSkeleton() {
  return (
    <div className="animate-pulse rounded-xl border border-gray-200 p-4">
      <div className="h-4 w-2/3 rounded bg-gray-200 mb-3" />
      <div className="h-3 w-full rounded bg-gray-100 mb-2" />
      <div className="h-3 w-4/5 rounded bg-gray-100" />
    </div>
  );
}

// Empty state
export function EmptyState({ title, description, action }: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center py-16 text-center">
      <div className="rounded-full bg-gray-100 p-4 dark:bg-gray-800 mb-4">
        <InboxIcon className="h-8 w-8 text-gray-400" />
      </div>
      <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3>
      <p className="mt-2 text-sm text-gray-500 max-w-sm">{description}</p>
      {action && <div className="mt-6">{action}</div>}
    </div>
  );
}

Animations with Framer Motion

// Page transition
const pageVariants = {
  initial: { opacity: 0, y: 8 },
  animate: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: -8 },
};

export function AnimatedPage({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      variants={pageVariants}
      initial="initial"
      animate="animate"
      exit="exit"
      transition={{ duration: 0.2, ease: 'easeOut' }}
    >
      {children}
    </motion.div>
  );
}

// Staggered list
const container = { animate: { transition: { staggerChildren: 0.05 } } };
const item = { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 } };

Accessibility Rules

// ✅ ARIA labels on all interactive elements
<button aria-label="Close dialog" onClick={onClose}>
  <XIcon aria-hidden="true" />
</button>

// ✅ Form accessibility
<label htmlFor="email" className="block text-sm font-medium">Email</label>
<input
  id="email"
  type="email"
  aria-describedby={errors.email ? 'email-error' : undefined}
  aria-invalid={!!errors.email}
/>
{errors.email && <p id="email-error" role="alert" className="text-sm text-red-600">{errors.email}</p>}

// ✅ Focus management in modals
<Dialog>
  <DialogContent onOpenAutoFocus={e => e.preventDefault()}>
    {/* First focusable: close button */}
    <DialogClose ref={closeRef} />
  </DialogContent>
</Dialog>

// ✅ Keyboard navigation
<div role="listbox" aria-labelledby="list-label">
  {options.map(opt => (
    <div
      key={opt.id}
      role="option"
      aria-selected={selected === opt.id}
      tabIndex={0}
      onKeyDown={e => e.key === 'Enter' && onSelect(opt.id)}
    >
      {opt.label}
    </div>
  ))}
</div>

Design Checklist

  • Mobile-first responsive (320px → 1536px)
  • Dark mode support
  • Focus styles visible (not outline: none without alternative)
  • Color contrast ≥ 4.5:1 (text), ≥ 3:1 (large text)
  • Loading states for all async operations
  • Empty states for all lists
  • Error states with helpful messages
  • Hover/active/disabled states on interactive elements
  • Reduced motion query (prefers-reduced-motion)
  • Touch targets ≥ 44×44px on mobile
Weekly Installs
6
GitHub Stars
4
First Seen
5 days ago
Installed on
opencode6
gemini-cli6
claude-code6
github-copilot6
codex6
amp6