frontend-ui-engineering

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Frontend UI Engineering

前端UI工程

Overview

概述

Build production-quality user interfaces that are accessible, performant, and visually polished. The goal is UI that looks like it was built by a design-aware engineer at a top company — not like it was generated by an AI. This means real design system adherence, proper accessibility, thoughtful interaction patterns, and no generic "AI aesthetic."
构建可访问、性能优异且视觉精美的生产级用户界面。目标是打造出如同顶尖公司中懂设计的工程师所构建的UI——而非AI生成的风格。这意味着要严格遵循真实的设计系统、保证良好的可访问性、采用深思熟虑的交互模式,并且摒弃通用的“AI审美”。

When to Use

适用场景

  • Building new UI components or pages
  • Modifying existing user-facing interfaces
  • Implementing responsive layouts
  • Adding interactivity or state management
  • Fixing visual or UX issues
  • 构建新的UI组件或页面
  • 修改现有的面向用户界面
  • 实现响应式布局
  • 添加交互功能或状态管理
  • 修复视觉或UX问题

Component Architecture

组件架构

File Structure

文件结构

Colocate everything related to a component:
src/components/
  TaskList/
    TaskList.tsx          # Component implementation
    TaskList.test.tsx     # Tests
    TaskList.stories.tsx  # Storybook stories (if using)
    use-task-list.ts      # Custom hook (if complex state)
    types.ts              # Component-specific types (if needed)
将与组件相关的所有内容放在一起:
src/components/
  TaskList/
    TaskList.tsx          # 组件实现
    TaskList.test.tsx     # 测试文件
    TaskList.stories.tsx  # Storybook故事(如果使用)
    use-task-list.ts      # 自定义Hook(如果涉及复杂状态)
    types.ts              # 组件专属类型(如有需要)

Component Patterns

组件模式

Prefer composition over configuration:
tsx
// Good: Composable
<Card>
  <CardHeader>
    <CardTitle>Tasks</CardTitle>
  </CardHeader>
  <CardBody>
    <TaskList tasks={tasks} />
  </CardBody>
</Card>

// Avoid: Over-configured
<Card
  title="Tasks"
  headerVariant="large"
  bodyPadding="md"
  content={<TaskList tasks={tasks} />}
/>
Keep components focused:
tsx
// Good: Does one thing
export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
  return (
    <li className="flex items-center gap-3 p-3">
      <Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
      <span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
      <Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
        <TrashIcon />
      </Button>
    </li>
  );
}
Separate data fetching from presentation:
tsx
// Container: handles data
export function TaskListContainer() {
  const { tasks, isLoading, error } = useTasks();

  if (isLoading) return <TaskListSkeleton />;
  if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
  if (tasks.length === 0) return <EmptyState message="No tasks yet" />;

  return <TaskList tasks={tasks} />;
}

// Presentation: handles rendering
export function TaskList({ tasks }: { tasks: Task[] }) {
  return (
    <ul role="list" className="divide-y">
      {tasks.map(task => <TaskItem key={task.id} task={task} />)}
    </ul>
  );
}
优先使用组合而非配置:
tsx
// 推荐:可组合式
<Card>
  <CardHeader>
    <CardTitle>Tasks</CardTitle>
  </CardHeader>
  <CardBody>
    <TaskList tasks={tasks} />
  </CardBody>
</Card>

// 避免:过度配置
<Card
  title="Tasks"
  headerVariant="large"
  bodyPadding="md"
  content={<TaskList tasks={tasks} />}
/>
保持组件职责单一:
tsx
// 推荐:只做一件事
export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
  return (
    <li className="flex items-center gap-3 p-3">
      <Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
      <span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
      <Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
        <TrashIcon />
      </Button>
    </li>
  );
}
将数据获取与展示分离:
tsx
// 容器组件:处理数据
export function TaskListContainer() {
  const { tasks, isLoading, error } = useTasks();

  if (isLoading) return <TaskListSkeleton />;
  if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
  if (tasks.length === 0) return <EmptyState message="No tasks yet" />;

  return <TaskList tasks={tasks} />;
}

// 展示组件:负责渲染
export function TaskList({ tasks }: { tasks: Task[] }) {
  return (
    <ul role="list" className="divide-y">
      {tasks.map(task => <TaskItem key={task.id} task={task} />)}
    </ul>
  );
}

State Management

状态管理

Choose the simplest approach that works:
Local state (useState)           → Component-specific UI state
Lifted state                     → Shared between 2-3 sibling components
Context                          → Theme, auth, locale (read-heavy, write-rare)
URL state (searchParams)         → Filters, pagination, shareable UI state
Server state (React Query, SWR)  → Remote data with caching
Global store (Zustand, Redux)    → Complex client state shared app-wide
Avoid prop drilling deeper than 3 levels. If you're passing props through components that don't use them, introduce context or restructure the component tree.
选择最简单可行的方案:
本地状态(useState)           → 组件专属UI状态
提升状态(Lifted state)       → 在2-3个兄弟组件间共享
上下文(Context)              → 主题、权限、语言环境(读多写少场景)
URL状态(searchParams)         → 筛选器、分页、可分享的UI状态
服务端状态(React Query、SWR)  → 带缓存的远程数据
全局状态库(Zustand、Redux)    → 应用内共享的复杂客户端状态
避免prop drilling超过3层。 如果你需要通过不使用这些props的组件来传递props,引入上下文或重构组件树。

Design System Adherence

遵循设计系统

Avoid the AI Aesthetic

摒弃AI审美

AI-generated UI has recognizable patterns. Avoid all of them:
AI DefaultProduction Quality
Purple/indigo everythingUse the project's actual color palette
Excessive gradientsFlat or subtle gradients matching the design system
Rounded everything (rounded-2xl)Consistent border-radius from the design system
Generic hero sectionsContent-first layouts
Lorem ipsum-style copyRealistic placeholder content
Oversized padding everywhereConsistent spacing scale
Stock card gridsPurpose-driven layouts
Shadow-heavy designSubtle or no shadows unless the design system specifies
AI生成的UI有可识别的模式。要避免所有这些模式:
AI默认风格生产级质感风格
全是紫色/靛蓝色使用项目实际的调色板
过度使用渐变采用符合设计系统的扁平或细微渐变
所有元素都极度圆角(rounded-2xl)遵循设计系统中统一的圆角半径
通用的hero区域以内容为核心的布局
Lorem ipsum类占位文本真实的占位内容
各处都有超大内边距遵循统一的间距规范
通用卡片网格有明确目的的布局
大量使用阴影除非设计系统指定,否则使用细微阴影或无阴影

Spacing and Layout

间距与布局

Use a consistent spacing scale. Don't invent values:
css
/* Use the scale: 0.25rem increments (or whatever the project uses) */
/* Good */  padding: 1rem;      /* 16px */
/* Good */  gap: 0.75rem;       /* 12px */
/* Bad */   padding: 13px;      /* Not on any scale */
/* Bad */   margin-top: 2.3rem; /* Not on any scale */
使用统一的间距规范,不要随意自定义数值:
css
/* 使用规范:0.25rem递增(或项目采用的其他规范) */
/* 推荐 */  padding: 1rem;      /* 16px */
/* 推荐 */  gap: 0.75rem;       /* 12px */
/* 避免 */   padding: 13px;      /* 不在任何规范内 */
/* 避免 */   margin-top: 2.3rem; /* 不在任何规范内 */

Typography

排版

Respect the type hierarchy:
h1 → Page title (one per page)
h2 → Section title
h3 → Subsection title
body → Default text
small → Secondary/helper text
Don't skip heading levels. Don't use heading styles for non-heading content.
遵循字体层级:
h1 → 页面标题(每页一个)
h2 → 章节标题
h3 → 子章节标题
body → 默认文本
small → 次要/辅助文本
不要跳过标题层级。不要将标题样式用于非标题内容。

Color

颜色

  • Use semantic color tokens:
    text-primary
    ,
    bg-surface
    ,
    border-default
    — not raw hex values
  • Ensure sufficient contrast (4.5:1 for normal text, 3:1 for large text)
  • Don't rely solely on color to convey information (use icons, text, or patterns too)
  • 使用语义化颜色令牌:
    text-primary
    bg-surface
    border-default
    — 而非原始十六进制值
  • 确保足够的对比度(普通文本4.5:1,大文本3:1)
  • 不要仅依赖颜色来传递信息(同时使用图标、文本或图案)

Accessibility (WCAG 2.1 AA)

无障碍访问(WCAG 2.1 AA)

Every component must meet these standards:
每个组件都必须满足这些标准:

Keyboard Navigation

键盘导航

tsx
// Every interactive element must be keyboard accessible
<button onClick={handleClick}>Click me</button>        // ✓ Focusable by default
<div onClick={handleClick}>Click me</div>               // ✗ Not focusable
<div role="button" tabIndex={0} onClick={handleClick}    // ✓ But prefer <button>
     onKeyDown={e => e.key === 'Enter' && handleClick()}>
  Click me
</div>
tsx
// 所有交互元素都必须支持键盘访问
<button onClick={handleClick}>Click me</button>        // ✓ 默认可获取焦点
<div onClick={handleClick}>Click me</div>               // ✗ 无法获取焦点
<div role="button" tabIndex={0} onClick={handleClick}    // ✓ 但优先使用<button>
     onKeyDown={e => e.key === 'Enter' && handleClick()}>
  Click me
</div>

ARIA Labels

ARIA标签

tsx
// Label interactive elements that lack visible text
<button aria-label="Close dialog"><XIcon /></button>

// Label form inputs
<label htmlFor="email">Email</label>
<input id="email" type="email" />

// Or use aria-label when no visible label exists
<input aria-label="Search tasks" type="search" />
tsx
// 为没有可见文本的交互元素添加标签
<button aria-label="Close dialog"><XIcon /></button>

// 为表单输入框添加标签
<label htmlFor="email">Email</label>
<input id="email" type="email" />

// 当没有可见标签时,使用aria-label
<input aria-label="Search tasks" type="search" />

Focus Management

焦点管理

tsx
// Move focus when content changes
function Dialog({ isOpen, onClose }: DialogProps) {
  const closeRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (isOpen) closeRef.current?.focus();
  }, [isOpen]);

  // Trap focus inside dialog when open
  return (
    <dialog open={isOpen}>
      <button ref={closeRef} onClick={onClose}>Close</button>
      {/* dialog content */}
    </dialog>
  );
}
tsx
// 当内容变化时移动焦点
function Dialog({ isOpen, onClose }: DialogProps) {
  const closeRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (isOpen) closeRef.current?.focus();
  }, [isOpen]);

  // 当对话框打开时,将焦点限制在内部
  return (
    <dialog open={isOpen}>
      <button ref={closeRef} onClick={onClose}>Close</button>
      {/* 对话框内容 */}
    </dialog>
  );
}

Meaningful Empty and Error States

有意义的空状态和错误状态

tsx
// Don't show blank screens
function TaskList({ tasks }: { tasks: Task[] }) {
  if (tasks.length === 0) {
    return (
      <div role="status" className="text-center py-12">
        <TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
        <h3 className="mt-2 text-sm font-medium">No tasks</h3>
        <p className="mt-1 text-sm text-muted">Get started by creating a new task.</p>
        <Button className="mt-4" onClick={onCreateTask}>Create Task</Button>
      </div>
    );
  }

  return <ul role="list">...</ul>;
}
tsx
// 不要显示空白屏幕
function TaskList({ tasks }: { tasks: Task[] }) {
  if (tasks.length === 0) {
    return (
      <div role="status" className="text-center py-12">
        <TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
        <h3 className="mt-2 text-sm font-medium">No tasks</h3>
        <p className="mt-1 text-sm text-muted">Get started by creating a new task.</p>
        <Button className="mt-4" onClick={onCreateTask}>Create Task</Button>
      </div>
    );
  }

  return <ul role="list">...</ul>;
}

Responsive Design

响应式设计

Design for mobile first, then expand:
tsx
// Tailwind: mobile-first responsive
<div className="
  grid grid-cols-1      /* Mobile: single column */
  sm:grid-cols-2        /* Small: 2 columns */
  lg:grid-cols-3        /* Large: 3 columns */
  gap-4
">
Test at these breakpoints: 320px, 768px, 1024px, 1440px.
先为移动端设计,再扩展到桌面端:
tsx
// Tailwind:移动端优先的响应式设计
<div className="
  grid grid-cols-1      /* 移动端:单列 */
  sm:grid-cols-2        /* 小屏:2列 */
  lg:grid-cols-3        /* 大屏:3列 */
  gap-4
">
在以下断点测试:320px、768px、1024px、1440px。

Loading and Transitions

加载与过渡

tsx
// Skeleton loading (not spinners for content)
function TaskListSkeleton() {
  return (
    <div className="space-y-3" aria-busy="true" aria-label="Loading tasks">
      {Array.from({ length: 3 }).map((_, i) => (
        <div key={i} className="h-12 bg-muted animate-pulse rounded" />
      ))}
    </div>
  );
}

// Optimistic updates for perceived speed
function useToggleTask() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: toggleTask,
    onMutate: async (taskId) => {
      await queryClient.cancelQueries({ queryKey: ['tasks'] });
      const previous = queryClient.getQueryData(['tasks']);

      queryClient.setQueryData(['tasks'], (old: Task[]) =>
        old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
      );

      return { previous };
    },
    onError: (_err, _taskId, context) => {
      queryClient.setQueryData(['tasks'], context?.previous);
    },
  });
}
tsx
// 骨架屏加载(内容加载不要使用加载动画)
function TaskListSkeleton() {
  return (
    <div className="space-y-3" aria-busy="true" aria-label="Loading tasks">
      {Array.from({ length: 3 }).map((_, i) => (
        <div key={i} className="h-12 bg-muted animate-pulse rounded" />
      ))}
    </div>
  );
}

// 乐观更新提升感知速度
function useToggleTask() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: toggleTask,
    onMutate: async (taskId) => {
      await queryClient.cancelQueries({ queryKey: ['tasks'] });
      const previous = queryClient.getQueryData(['tasks']);

      queryClient.setQueryData(['tasks'], (old: Task[]) =>
        old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
      );

      return { previous };
    },
    onError: (_err, _taskId, context) => {
      queryClient.setQueryData(['tasks'], context?.previous);
    },
  });
}

Common Rationalizations

常见自我辩解与实际情况

RationalizationReality
"Accessibility is a nice-to-have"It's a legal requirement in many jurisdictions and an engineering quality standard.
"We'll make it responsive later"Retrofitting responsive design is 3x harder than building it from the start.
"The design isn't final, so I'll skip styling"Use the design system defaults. Unstyled UI creates a broken first impression for reviewers.
"This is just a prototype"Prototypes become production code. Build the foundation right.
"The AI aesthetic is fine for now"It signals low quality. Use the project's actual design system from the start.
自我辩解实际情况
“无障碍访问是锦上添花的功能”在许多地区这是法律要求,同时也是工程质量标准。
“我们之后再做响应式适配”后期改造响应式设计的难度是从一开始就构建的3倍。
“设计还没定稿,所以我先不做样式”使用设计系统的默认样式。无样式的UI会给评审者留下糟糕的第一印象。
“这只是个原型”原型往往会变成生产代码。从一开始就打好基础。
“AI审美现在凑合用就行”这会传递出低质量的信号。从一开始就使用项目实际的设计系统。

Red Flags

危险信号

  • Components with more than 200 lines (split them)
  • Inline styles or arbitrary pixel values
  • Missing error states, loading states, or empty states
  • No keyboard navigation testing
  • Color as the sole indicator of state (red/green without text or icons)
  • Generic "AI look" (purple gradients, oversized cards, stock layouts)
  • 组件代码超过200行(拆分它)
  • 内联样式或任意像素值
  • 缺少错误状态、加载状态或空状态
  • 未测试键盘导航
  • 仅用颜色表示状态(红/绿但无文本或图标辅助)
  • 通用的“AI风格”(紫色渐变、超大卡片、通用布局)

Verification

验证

After building UI:
  • Component renders without console errors
  • All interactive elements are keyboard accessible (Tab through the page)
  • Screen reader can convey the page's content and structure
  • Responsive: works at 320px, 768px, 1024px, 1440px
  • Loading, error, and empty states all handled
  • Follows the project's design system (spacing, colors, typography)
  • No accessibility warnings in dev tools or axe-core
构建完UI后:
  • 组件渲染时控制台无错误
  • 所有交互元素都可通过键盘访问(按Tab键遍历页面)
  • 屏幕阅读器可以传达页面的内容和结构
  • 响应式:在320px、768px、1024px、1440px尺寸下均可正常工作
  • 已处理加载、错误和空状态
  • 遵循项目的设计系统(间距、颜色、排版)
  • 开发工具或axe-core中无无障碍访问警告