AgentSkillsCN

ux-ui-design

为布局、层级结构与交互流程提供 UX 与 UI 设计指导。

SKILL.md
--- frontmatter
name: ux-ui-design
description: UX and UI design guidance for layout, hierarchy, and interaction flows.
argument-hint: Ask for IA, layout, or interaction design guidance.

UX and UI Design

Use this skill for interface structure, user flows, visual hierarchy, and interaction design. Good UX/UI makes features intuitive and delightful to use.


Table of Contents

  1. Design Principles
  2. Layout Patterns
  3. Visual Hierarchy
  4. Interaction Design
  5. Responsive Design
  6. Common Patterns
  7. Anti-Patterns to Avoid

Design Principles

1. Clarity Over Cleverness

Users should immediately understand what they can do and how to do it.

typescript
// ❌ UNCLEAR: What does this do?
<button className="p-4 rounded-full bg-gradient-to-r from-purple-500 to-pink-500">
  <SparklesIcon />
</button>

// ✅ CLEAR: Action is obvious
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
  Create New Post
  <PlusIcon className="ml-2 w-4 h-4" aria-hidden="true" />
</button>

2. Consistency

Use the same patterns throughout the application.

ElementPatternExample
Primary actionsBlue filled button"Save Changes"
Secondary actionsGray outline button"Cancel"
Destructive actionsRed filled button"Delete Account"
Danger warningsRed background + icon⚠️ "This action cannot be undone"
Success messagesGreen background + icon✅ "Profile saved!"

3. Progressive Disclosure

Show only what's needed, when it's needed.

typescript
// ❌ OVERWHELMING: All options at once
<form>
  <input placeholder="Name" />
  <input placeholder="Email" />
  <input placeholder="Phone" />
  <input placeholder="Address" />
  <input placeholder="City" />
  <input placeholder="State" />
  <input placeholder="Zip" />
  <select>{/* 50 country options */}</select>
  <textarea placeholder="Bio (optional, max 500 chars)" rows={10} />
  {/* 20 more fields... */}
  <button>Submit</button>
</form>

// ✅ PROGRESSIVE: Multi-step form
<FormStep current={1} total={3}>
  <h2>Let's start with your basic info</h2>
  <input placeholder="Name" />
  <input placeholder="Email" />
  <button>Next Step</button>
</FormStep>

4. Feedback & Response

Always acknowledge user actions immediately.

typescript
// ❌ NO FEEDBACK: User wonders if click worked
<button onClick={saveProfile}>
  Save Profile
</button>

// ✅ IMMEDIATE FEEDBACK: User knows action is processing
<button
  onClick={saveProfile}
  disabled={isSaving}
  className={isSaving ? 'opacity-50 cursor-not-allowed' : ''}
>
  {isSaving ? (
    <>
      <Spinner className="mr-2" />
      Saving...
    </>
  ) : (
    'Save Profile'
  )}
</button>

Layout Patterns

Container Widths

typescript
// Standard responsive container
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
  {/* Content */}
</div>

// Reading-optimized (narrower for text)
<article className="max-w-2xl mx-auto px-4">
  {/* Long-form content */}
</article>

// Full-width (dashboards, tables)
<div className="w-full p-4">
  {/* Wide content */}
</div>

Grid Layouts

typescript
// Responsive grid (1 col mobile, 2 col tablet, 3 col desktop)
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  {items.map(item => (
    <Card key={item.id} {...item} />
  ))}
</div>

// Auto-fit grid (fills available space)
<div className="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
  {items.map(item => (
    <Card key={item.id} {...item} />
  ))}
</div>

Sidebar Layouts

typescript
// Responsive sidebar (stacks on mobile)
<div className="flex flex-col lg:flex-row gap-6">
  {/* Sidebar */}
  <aside className="w-full lg:w-64 flex-shrink-0">
    <FilterPanel />
  </aside>

  {/* Main content */}
  <main className="flex-1 min-w-0">
    <ResultsList />
  </main>
</div>

Visual Hierarchy

Typography Scale

typescript
// Heading hierarchy
<h1 className="text-4xl font-bold">Page Title</h1>
<h2 className="text-3xl font-bold mt-8">Section Heading</h2>
<h3 className="text-2xl font-semibold mt-6">Subsection</h3>
<h4 className="text-xl font-medium mt-4">Card Heading</h4>

// Body text
<p className="text-base leading-7">
  Standard paragraph text with comfortable line height (175%).
</p>

// Small text (metadata, captions)
<span className="text-sm text-gray-600">
  Posted 2 hours ago
</span>

Spacing System

typescript
// Consistent spacing scale (4px base unit)
const spacing = {
  xs: '0.25rem',  // 4px  - Tight spacing
  sm: '0.5rem',   // 8px  - Small gaps
  md: '1rem',     // 16px - Default spacing
  lg: '1.5rem',   // 24px - Section spacing
  xl: '2rem',     // 32px - Large gaps
  '2xl': '3rem',  // 48px - Major sections
};

// Usage
<div className="space-y-4">  {/* 16px vertical gaps */}
  <Card />
  <Card />
  <Card />
</div>

<section className="mb-12">  {/* 48px margin bottom */}
  {/* Section content */}
</section>

Color Hierarchy

typescript
// Text hierarchy
<div>
  <h2 className="text-gray-900">Primary Heading</h2>
  <p className="text-gray-700">Body text</p>
  <span className="text-gray-500">Secondary text</span>
  <small className="text-gray-400">Tertiary text</small>
</div>

// Interactive elements
<button className="bg-blue-500 hover:bg-blue-600 active:bg-blue-700">
  Primary Action
</button>

<button className="bg-gray-200 hover:bg-gray-300 active:bg-gray-400">
  Secondary Action
</button>

Interaction Design

Button States

typescript
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'destructive';
  isLoading?: boolean;
  disabled?: boolean;
}

export default function Button({ variant, isLoading, disabled, children }: ButtonProps) {
  const baseStyles = 'px-4 py-2 rounded-lg font-medium transition-colors focus:ring-2 focus:ring-offset-2';

  const variantStyles = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500 disabled:bg-blue-300',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500 disabled:bg-gray-100',
    destructive: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-500 disabled:bg-red-300',
  };

  return (
    <button
      className={`${baseStyles} ${variantStyles[variant]}`}
      disabled={disabled || isLoading}
    >
      {isLoading ? (
        <>
          <Spinner className="mr-2 w-4 h-4 animate-spin" />
          Loading...
        </>
      ) : (
        children
      )}
    </button>
  );
}

Form Input States

typescript
// Visual states for form inputs
<input
  type="text"
  className={cn(
    'w-full px-4 py-2 rounded-lg border transition-colors',
    {
      // Default
      'border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500': !error,
      // Error
      'border-red-500 focus:border-red-500 focus:ring-2 focus:ring-red-500': error,
      // Disabled
      'bg-gray-100 cursor-not-allowed': disabled,
    }
  )}
  disabled={disabled}
  aria-invalid={error ? 'true' : 'false'}
  aria-describedby={error ? 'error-message' : undefined}
/>

{error && (
  <p id="error-message" className="text-sm text-red-600 mt-1" role="alert">
    {error}
  </p>
)}

Loading Patterns

typescript
// Skeleton loading (best for known layouts)
<div className="animate-pulse space-y-4">
  <div className="h-8 bg-gray-200 rounded w-3/4"></div>
  <div className="h-4 bg-gray-200 rounded w-full"></div>
  <div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>

// Spinner (unknown content)
<div className="flex items-center justify-center p-8">
  <Spinner className="w-8 h-8 animate-spin text-blue-500" />
  <span className="ml-3 text-gray-600">Loading...</span>
</div>

// Progressive loading (show partial content)
<div>
  {partialData.length > 0 && (
    <div className="opacity-50">
      {partialData.map(item => <Item key={item.id} {...item} />)}
    </div>
  )}
  {isLoading && <Spinner />}
</div>

Responsive Design

Mobile-First Approach

typescript
// ❌ DESKTOP-FIRST: Hard to simplify for mobile
<nav className="flex gap-8 text-lg px-12">
  <Link>Dashboard</Link>
  <Link>Projects</Link>
  <Link>Team</Link>
  <Link>Settings</Link>
  <Link>Analytics</Link>
  <Link>Reports</Link>
</nav>

// ✅ MOBILE-FIRST: Start simple, enhance for desktop
<nav className="relative">
  {/* Mobile: Hamburger menu */}
  <button
    className="lg:hidden p-2"
    onClick={() => setMenuOpen(!menuOpen)}
    aria-label="Toggle menu"
  >
    <MenuIcon />
  </button>

  {/* Mobile: Drawer */}
  {menuOpen && (
    <div className="absolute top-full left-0 w-full bg-white shadow-lg lg:hidden">
      <Link className="block px-4 py-3 hover:bg-gray-50">Dashboard</Link>
      <Link className="block px-4 py-3 hover:bg-gray-50">Projects</Link>
      {/* ... */}
    </div>
  )}

  {/* Desktop: Horizontal */}
  <div className="hidden lg:flex gap-6">
    <Link>Dashboard</Link>
    <Link>Projects</Link>
    {/* ... */}
  </div>
</nav>

Breakpoint Guidelines

typescript
// Tailwind breakpoints
const breakpoints = {
  sm: '640px',  // Large phones
  md: '768px',  // Tablets
  lg: '1024px', // Small desktops
  xl: '1280px', // Large desktops
  '2xl': '1536px', // Extra large
};

// Usage
<div className="
  p-4          {/* Mobile: 16px padding */}
  md:p-6       {/* Tablet: 24px padding */}
  lg:p-8       {/* Desktop: 32px padding */}
">
  <h1 className="
    text-2xl       {/* Mobile: 24px */}
    md:text-3xl    {/* Tablet: 30px */}
    lg:text-4xl    {/* Desktop: 36px */}
  ">
    Responsive Heading
  </h1>
</div>

Common Patterns

Card Design

typescript
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
  {/* Image */}
  <img
    src={post.image}
    alt={post.title}
    className="w-full h-48 object-cover"
  />

  {/* Content */}
  <div className="p-6">
    {/* Category badge */}
    <span className="inline-block px-3 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full mb-3">
      {post.category}
    </span>

    {/* Title */}
    <h3 className="text-xl font-bold mb-2">
      {post.title}
    </h3>

    {/* Excerpt */}
    <p className="text-gray-600 mb-4 line-clamp-3">
      {post.excerpt}
    </p>

    {/* Metadata */}
    <div className="flex items-center text-sm text-gray-500">
      <Avatar src={post.author.avatar} />
      <span className="ml-2">{post.author.name}</span>
      <span className="mx-2">·</span>
      <time dateTime={post.publishedAt}>
        {formatDate(post.publishedAt)}
      </time>
    </div>
  </div>
</div>

Modal Dialog

typescript
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
  {/* Backdrop */}
  <div className="fixed inset-0 bg-black/50" aria-hidden="true" />

  {/* Modal container */}
  <div className="fixed inset-0 flex items-center justify-center p-4">
    {/* Modal panel */}
    <Dialog.Panel className="bg-white rounded-lg max-w-md w-full p-6 shadow-xl">
      {/* Header */}
      <div className="flex items-start justify-between mb-4">
        <Dialog.Title className="text-xl font-bold">
          {title}
        </Dialog.Title>
        <button
          onClick={() => setIsOpen(false)}
          className="text-gray-400 hover:text-gray-600"
          aria-label="Close dialog"
        >
          <XIcon className="w-6 h-6" />
        </button>
      </div>

      {/* Content */}
      <Dialog.Description className="text-gray-600 mb-6">
        {description}
      </Dialog.Description>

      {/* Actions */}
      <div className="flex gap-3 justify-end">
        <button
          onClick={() => setIsOpen(false)}
          className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
        >
          Cancel
        </button>
        <button
          onClick={handleConfirm}
          className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
        >
          Confirm
        </button>
      </div>
    </Dialog.Panel>
  </div>
</Dialog>

Data Table

typescript
<div className="overflow-x-auto">
  <table className="min-w-full divide-y divide-gray-200">
    <thead className="bg-gray-50">
      <tr>
        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Name
        </th>
        <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Status
        </th>
        <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
          Actions
        </th>
      </tr>
    </thead>
    <tbody className="bg-white divide-y divide-gray-200">
      {items.map(item => (
        <tr key={item.id} className="hover:bg-gray-50">
          <td className="px-6 py-4 whitespace-nowrap font-medium">
            {item.name}
          </td>
          <td className="px-6 py-4 whitespace-nowrap">
            <span className={cn(
              'px-2 py-1 text-xs font-medium rounded-full',
              item.status === 'active' && 'bg-green-100 text-green-800',
              item.status === 'pending' && 'bg-yellow-100 text-yellow-800',
              item.status === 'inactive' && 'bg-gray-100 text-gray-800',
            )}>
              {item.status}
            </span>
          </td>
          <td className="px-6 py-4 whitespace-nowrap text-right">
            <button className="text-blue-600 hover:text-blue-900">
              Edit
            </button>
            <button className="ml-4 text-red-600 hover:text-red-900">
              Delete
            </button>
          </td>
        </tr>
      ))}
    </tbody>
  </table>
</div>

Anti-Patterns to Avoid

❌ Low Contrast

typescript
// ❌ BAD: Light gray text on white (fails WCAG AA)
<p className="text-gray-300">
  This text is hard to read
</p>

// ✅ GOOD: Sufficient contrast (>4.5:1)
<p className="text-gray-700">
  This text is easy to read
</p>

❌ Tiny Touch Targets

typescript
// ❌ BAD: Too small for mobile (< 44px)
<button className="p-1 text-xs">
  <TrashIcon className="w-3 h-3" />
</button>

// ✅ GOOD: Minimum 44x44px touch target
<button className="p-3">
  <TrashIcon className="w-5 h-5" aria-label="Delete" />
</button>

❌ No Visual Feedback

typescript
// ❌ BAD: No hover state, no disabled state
<button onClick={save}>
  Save
</button>

// ✅ GOOD: Clear states
<button
  onClick={save}
  disabled={isDisabled}
  className="
    bg-blue-500 text-white
    hover:bg-blue-600
    active:scale-95
    disabled:opacity-50 disabled:cursor-not-allowed
    transition-all
  "
>
  Save
</button>

❌ Inconsistent Patterns

typescript
// ❌ BAD: Different patterns for the same action
<button className="bg-blue-500 text-white">Create Post</button>
<button className="border border-green-500 text-green-500">Add Comment</button>
<a href="/new" className="text-purple-600 underline">New Project</a>

// ✅ GOOD: Consistent primary action pattern
<button className="bg-blue-500 text-white">Create Post</button>
<button className="bg-blue-500 text-white">Add Comment</button>
<button className="bg-blue-500 text-white">New Project</button>

Design Checklist

Before shipping a UI feature:

  • Visual hierarchy is clear (most important element stands out)
  • Color contrast meets WCAG AA (4.5:1 for text)
  • Touch targets are at least 44x44px on mobile
  • Loading states are shown for async operations
  • Error states are user-friendly and actionable
  • Empty states guide users to next action
  • Focus indicators are visible (2px ring)
  • Hover states provide feedback
  • Disabled states are visually distinct
  • Mobile layout works down to 320px width
  • Typography scale is consistent
  • Spacing follows design system
  • Interactions feel responsive (< 100ms feedback)

References


Skill Status: Enhanced Last Updated: 2026-02-13 (Phase 5)