关于 frontend-ui-engineering
面向工程师和编码 Agent 的界面开发技能,涵盖组件组织、状态管理、设计系统、无障碍与响应式实践,并提供实现后的验证要点。
主要功能
- 01组件组合与组织建议
- 02状态管理选择指南
- 03遵循设计系统的视觉规范
- 04WCAG 2.1 AA 无障碍实践
- 05响应式布局与界面验证
适用场景
- 01开发新的界面页面或组件
- 02改善现有界面的体验
- 03实现响应式交互
- 04检查无障碍和界面状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ addyosmani/ agent-skills --skill frontend-ui-engineering安装后在 Claude Code 中输入/frontend-ui-engineering确认已加载。
详细介绍
前端 UI 工程
概述
构建适用于生产环境的用户界面,确保无障碍、高性能且视觉精致。目标是打造看起来像由顶尖公司的设计意识强的工程师构建的 UI,而不是像 AI 生成的界面。这意味着要真正遵循设计系统、确保无障碍性、采用经过深思熟虑的交互模式,并避免千篇一律的“AI 美学”。
适用场景
- 构建新的 UI 组件或页面
- 修改现有的面向用户的界面
- 实现响应式布局
- 添加交互功能或状态管理
- 修复视觉或 UX 问题
组件架构
文件结构
将组件相关的所有内容放在一起:
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)
组件模式
优先采用组合,而非配置:
// 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} />}
/>
让组件各司其职:
// 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>
);
}
将数据获取与展示分离:
// Container: handles data
export function TaskListContainer() {
const { tasks, isLoading, error, refetch } = 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>
);
}
状态管理
选择可行的最简单方案:
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
避免超过 3 层的属性逐层传递。 如果你需要通过不使用这些属性的组件传递属性,请引入上下文或重新组织组件树。
遵循设计系统
以参考为导向的 UI 质量
当产品需要鲜明的视觉方向时,请先收集依据,再选择布局:
- 搜索可信的参考目录,或使用产品团队提供的参考。
- 研究两到三个相关界面。记录有关层级、信息密度、导航、控件、响应式行为和交互状态的设计决策。
- 在实施前,将这些决策整理成简短的设计约定。明确界面的用途、主要操作、必需状态、响应式规则以及应避免的模式。
- 使用产品自身的组件、设计令牌、内容和视觉语言,重新构建有用的结构。切勿照搬其他产品的品牌标识、专有文本、图像或精确布局。
将参考用作依据,而不是模板。如果无法获取参考,请记录假设,并根据产品现有的设计系统验证结果。
避免 AI 美学
AI 生成的 UI 有明显的惯用模式。请全部避免:
| AI 常见默认模式 | 问题所在 | 生产级质量标准 |
|---|---|---|
| 到处使用紫色/靛蓝色 | 模型默认选择视觉上“安全”的配色,导致每个应用看起来都一样 | 使用项目实际的调色板 |
| 过度使用渐变 | 渐变会增加视觉噪音,并与大多数设计系统冲突 | 使用符合设计系统的纯色或微妙渐变 |
| 所有元素都采用圆角(rounded-2xl) | 最大圆角传达出“亲和”的感觉,却忽视了真实设计中的圆角层级 | 使用设计系统中一致的 border-radius |
| 千篇一律的主视觉区块 | 模板化布局与实际内容或用户需求无关 | 以内容为先的布局 |
| 类似 Lorem ipsum 的文案 | 占位文本会掩盖真实内容才能暴露的布局问题(长度、换行、溢出) | 使用贴近实际的占位内容 |
| 所有地方都使用超大内边距 | 处处使用宽裕且相同的内边距会破坏视觉层级并浪费屏幕空间 | 使用一致的间距尺度 |
| 现成的卡片网格 | 统一网格是布局捷径,忽视了信息优先级和浏览模式 | 根据目的设计布局 |
| 过度依赖阴影的设计 | 多层阴影会增加景深感,与内容争夺注意力,并拖慢低端设备的渲染速度 | 除非设计系统另有规定,否则使用轻微阴影或不使用阴影 |
间距与布局
使用一致的间距尺度。不要随意创造数值:
/* 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 */
字体排版
遵循文字层级:
h1 → Page title (one per page)
h2 → Section title
h3 → Subsection title
body → Default text
small → Secondary/helper text
不要跳过标题级别。不要将标题样式用于非标题内容。
颜色
- 使用语义颜色令牌:
text-primary、bg-surface、border-default,而不是原始十六进制值 - 确保对比度充足(普通文本为 4.5:1,大号文本为 3:1)
- 不要仅依靠颜色传达信息(也要使用图标、文本或图案)
无障碍(WCAG 2.1 AA)
每个组件都必须符合以下标准:
键盘导航
// 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 => {
if (e.key === 'Enter') handleClick();
if (e.key === ' ') e.preventDefault();
}}
onKeyUp={e => {
if (e.key === ' ') handleClick();
}}>
Click me
</div>
ARIA 标签
// 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" />
焦点管理
// 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>
);
}
有意义的空状态和错误状态
// 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>;
}
响应式设计
先针对移动设备设计,再逐步扩展:
// 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
">
请在以下断点进行测试:320px、768px、1024px、1440px。
加载和过渡效果
// 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);
},
});
}
另请参阅
有关详细的无障碍要求和测试工具,请参阅 ../../references/accessibility-checklist.md。
常见的自我安慰
| 自我安慰 | 实际情况 |
|---|---|
| “无障碍只是锦上添花” | 在许多司法管辖区,无障碍是法律要求,也是工程质量标准。 |
| “我们之后再做响应式适配” | 事后改造响应式设计,比从一开始就构建难 3 倍。 |
| “设计还没定稿,所以我先不做样式” | 使用设计系统的默认设置。没有样式的 UI 会给评审者留下糟糕的第一印象。 |
| “这只是一个原型” | 原型会变成生产代码。请从一开始就打好基础。 |
| “暂时用 AI 风格也没关系” | 这会让人觉得质量低下。请从一开始就使用项目实际的设计系统。 |
危险信号
- 超过 200 行的组件(请拆分)
- 内联样式或随意指定的像素值
- 缺少错误状态、加载状态或空状态
- 未测试键盘导航
- 仅用颜色表示状态(只用红色/绿色,没有文本或图标)
- 通用的“AI 风格”(紫色渐变、尺寸过大的卡片、现成的布局)
验证
构建 UI 后:
- 组件渲染时没有控制台错误
- 所有交互元素都可通过键盘访问(使用 Tab 遍历页面)
- 屏幕阅读器能够传达页面内容和结构
- 响应式:在 320px、768px、1024px、1440px 下均可正常使用
- 在适用情况下,妥善处理加载、空、错误、成功和权限状态
- 遵循项目的设计系统(间距、颜色、排版)
- 渲染结果通过最终的 UI 专项完成度审查
- 开发者工具或 axe-core 中没有无障碍警告
评论
支持 Markdown