# Frontend UI Engineering

> Строит доступные адаптивные пользовательские интерфейсы продакшн-качества. Используй при создании или изменении интерфейсов и страниц, создании компонентов, вёрстке макетов, выполнении требований доступности WCAG, управлении состоянием или когда результат должен выглядеть и ощущаться как продакшн, а не как сгенерированный ИИ.

- Skill: `aleksandr-litvinenko/frontend-ui-engineering` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aleksandr-litvinenko/frontend-ui-engineering`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aleksandr-litvinenko/frontend-ui-engineering/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Aleksandr-Litvinenko (https://skillmd.com/u/aleksandr-litvinenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aleksandr-litvinenko/frontend-ui-engineering

---


# Фронтенд-инженерия интерфейсов

## Обзор

Строй пользовательские интерфейсы продакшн-качества: доступные, производительные и визуально доведённые. Цель — интерфейс, который выглядит так, будто его собрал инженер с дизайнерским чутьём в сильной компании, а не сгенерировал ИИ. Это значит: реальное следование дизайн-системе, настоящая доступность, продуманные паттерны взаимодействия и никакой обобщённой «ИИ-эстетики».

## Когда применять

- Строишь новые UI-компоненты или страницы
- Меняешь существующие пользовательские интерфейсы
- Верстаешь адаптивные макеты
- Добавляешь интерактивность или управление состоянием
- Чинишь визуальные или UX-проблемы

## Архитектура компонентов

### Структура файлов

Держи всё, относящееся к компоненту, рядом:

```
src/components/
  TaskList/
    TaskList.tsx          # Реализация компонента
    TaskList.test.tsx     # Тесты
    TaskList.stories.tsx  # Истории Storybook (если используется)
    use-task-list.ts      # Пользовательский хук (если состояние сложное)
    types.ts              # Типы, специфичные для компонента (если нужны)
```

### Паттерны компонентов

**Композиция важнее конфигурации:**

```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>
  );
}
```

## Управление состоянием

**Выбирай простейший подход, который работает:**

```
Локальное состояние (useState)      → UI-состояние конкретного компонента
Поднятое состояние                  → Общее для 2–3 соседних компонентов
Контекст                            → Тема, аутентификация, локаль (много чтения, редкая запись)
Состояние в URL (searchParams)      → Фильтры, пагинация, состояние UI, которым можно поделиться
Серверное состояние (React Query, SWR) → Удалённые данные с кешированием
Глобальное хранилище (Zustand, Redux) → Сложное клиентское состояние на всё приложение
```

**Не прокидывай пропсы глубже трёх уровней.** Если ты передаёшь пропсы через компоненты, которые их не используют, вводи контекст или перестраивай дерево компонентов.

## Следование дизайн-системе

### Избегай ИИ-эстетики

У интерфейсов, сгенерированных ИИ, есть узнаваемые признаки. Избегай их всех:

| Умолчание ИИ | В чём проблема | Продакшн-качество |
|---|---|---|
| Фиолетовый/индиго повсюду | Модели скатываются к визуально «безопасным» палитрам, и все приложения выглядят одинаково | Используй реальную палитру проекта |
| Избыток градиентов | Градиенты добавляют визуальный шум и конфликтуют с большинством дизайн-систем | Плоская заливка или мягкие градиенты в духе дизайн-системы |
| Скругление всего подряд (rounded-2xl) | Максимальное скругление сигналит «дружелюбно», но игнорирует иерархию радиусов в настоящих макетах | Согласованный border-radius из дизайн-системы |
| Обобщённые hero-блоки | Шаблонная вёрстка без связи с реальным содержимым и потребностью пользователя | Вёрстка от содержимого |
| Текст в духе lorem ipsum | Текст-заглушка прячет проблемы вёрстки, которые вскрывает реальный контент (длина, переносы, переполнение) | Реалистичный текст-заполнитель |
| Огромные отступы повсюду | Одинаково щедрые отступы уничтожают визуальную иерархию и расходуют место на экране | Согласованная шкала отступов |
| Стоковые сетки карточек | Однородные сетки — способ не думать о вёрстке, игнорирующий приоритет информации и то, как её просматривают | Вёрстка под задачу |
| Обилие теней | Слоистые тени добавляют глубину, конкурирующую с содержимым, и замедляют отрисовку на слабых устройствах | Мягкие тени или их отсутствие, если дизайн-система не требует иного |

### Отступы и вёрстка

Используй согласованную шкалу отступов. Не выдумывай значения:

```css
/* Пользуйся шкалой: шаг 0,25rem (или тем, что принято в проекте) */
/* Хорошо */  padding: 1rem;      /* 16px */
/* Хорошо */  gap: 0.75rem;       /* 12px */
/* Плохо */   padding: 13px;      /* Не из шкалы */
/* Плохо */   margin-top: 2.3rem; /* Не из шкалы */
```

### Типографика

Уважай иерархию заголовков:

```
h1 → Заголовок страницы (один на странице)
h2 → Заголовок раздела
h3 → Заголовок подраздела
body → Основной текст
small → Вторичный/вспомогательный текст
```

Не пропускай уровни заголовков. Не используй стили заголовков для не-заголовков.

### Цвет

- Используй семантические токены цвета: `text-primary`, `bg-surface`, `border-default`, а не голые hex-значения
- Обеспечь достаточный контраст (4,5:1 для обычного текста, 3:1 для крупного)
- Не полагайся только на цвет для передачи информации (добавляй иконки, текст или паттерны)

## Доступность (WCAG 2.1 AA)

Каждый компонент должен соответствовать этим требованиям:

### Навигация с клавиатуры

```tsx
// Каждый интерактивный элемент должен быть доступен с клавиатуры
<button onClick={handleClick}>Click me</button>        // ✓ Фокусируется по умолчанию
<div onClick={handleClick}>Click me</div>               // ✗ Не фокусируется
<div role="button" tabIndex={0} onClick={handleClick}    // ✓ Но лучше <button>
     onKeyDown={e => {
       if (e.key === 'Enter') handleClick();
       if (e.key === ' ') e.preventDefault();
     }}
     onKeyUp={e => {
       if (e.key === ' ') handleClick();
     }}>
  Click me
</div>
```

### ARIA-подписи

```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" />
```

### Управление фокусом

```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>
  );
}
```

### Осмысленные пустые состояния и состояния ошибок

```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>;
}
```

## Адаптивная вёрстка

Проектируй сначала под мобильные, потом расширяй:

```tsx
// Tailwind: адаптивность от мобильных
<div className="
  grid grid-cols-1      /* Мобильные: одна колонка */
  sm:grid-cols-2        /* Малые: 2 колонки */
  lg:grid-cols-3        /* Большие: 3 колонки */
  gap-4
">
```

Проверяй на этих контрольных точках: 320px, 768px, 1024px, 1440px.

## Загрузка и переходы

```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);
    },
  });
}
```

## См. также

Подробные требования к доступности и инструменты тестирования — см. `../../references/accessibility-checklist.md`.

## Типовые самооправдания

| Самооправдание | Как на самом деле |
|---|---|
| «Доступность — это приятное дополнение» | Во многих юрисдикциях это требование закона, а в инженерии — стандарт качества. |
| «Сделаем адаптив потом» | Прикрутить адаптивность задним числом втрое сложнее, чем заложить её сразу. |
| «Дизайн ещё не финальный, стилизацию пропущу» | Используй умолчания дизайн-системы. Нестилизованный интерфейс создаёт впечатление сломанного у тех, кто его смотрит. |
| «Это же просто прототип» | Прототипы становятся продакшном. Заложи фундамент правильно. |
| «ИИ-эстетика пока сойдёт» | Она сигналит о низком качестве. Используй реальную дизайн-систему проекта с самого начала. |

## Тревожные признаки

- Компоненты длиннее 200 строк (разбивай)
- Инлайновые стили или произвольные пиксельные значения
- Отсутствуют состояния ошибки, загрузки или пустоты
- Навигация с клавиатуры не проверялась
- Цвет как единственный индикатор состояния (красный/зелёный без текста и иконок)
- Обобщённый «вид от ИИ» (фиолетовые градиенты, огромные карточки, стоковая вёрстка)

## Проверка

После сборки интерфейса:

- [ ] Компонент отрисовывается без ошибок в консоли
- [ ] Все интерактивные элементы доступны с клавиатуры (пройди страницу по Tab)
- [ ] Скринридер способен передать содержание и структуру страницы
- [ ] Адаптивность: работает на 320px, 768px, 1024px, 1440px
- [ ] Состояния загрузки, ошибки и пустоты обработаны
- [ ] Соблюдена дизайн-система проекта (отступы, цвета, типографика)
- [ ] Нет предупреждений о доступности в devtools или axe-core

