Привет, Хабр! Меня зовут Яков, я UI Kit Lead в Exante. На работе мы развиваем свою корпоративную дизайн-систему — мощную, заточенную под конкретные требования, но закрытую. В свободное время я переосмыслил ряд решений из рабочей практики, переработал их под общие паттерны и собрал в open-source библиотеку — Vacano UI.

64 компонента, 17 form-обёрток, 1800+ иконок, 10 валидаторов, документация для людей и MCP-сервер для AI-ассистентов.

Философия

UI-библиотек много. Зачем ещё одна?

Большинство UI-китов делятся на два лагеря. Первые — headless: дают логику без стилей, а ты сам собирай внешний вид. Мощно, гибко, но это работа. Вторые — opinionated: дают красивые компоненты из коробки, но с жёсткой дизайн-системой, из которой сложно вырваться.

Vacano UI — посередине. Компоненты имеют готовый внешний вид и работают из коробки. Но каждый подэлемент доступен для стилизации через типизированные classname-слоты — без !important, без вложенных селекторов, без разглядывания DOM в инспекторе. Хочешь поменять цвет триггера у Select? Передай classnames={{ trigger: 'my-trigger' }} и пиши обычный CSS. TypeScript подскажет, какие слоты доступны у конкретного компонента.

Второй принцип — минимум церемоний для старта. Нет глобального ThemeProvider, нет createTheme, нет конфига с токенами, без которого ничего не заведётся. Поставил пакет, импортировал компонент, рендеришь. Провайдеры нужны только тем компонентам, которым они действительно необходимы: Confirmation, Notification, Toastr, SaveProgress, NotifyConfirmation — они работают через контекст и хуки, потому что управляют глобальным состоянием. Остальные 59 компонентов — автономны.

Третий принцип — каждый компонент доведён до конца. Не «вот вам <select> с классами, дальше сами», а полноценное решение задачи со всеми edge-кейсами, которые вылезают в продакшене. Дропдаун внутри модалки? Портал. Локализация дат? Intl.DateTimeFormat, ноль зависимостей. Ввод OTP на мобильной клавиатуре? Хак с maxLength. Ошибка валидации ломает верстку соседних полей? CSS Grid subgrid. Подробности по каждому компоненту — ниже.

Быстрый старт

pnpm add @vacano/ui @emotion/react @emotion/styled
import { GlobalStyle, Button, Input, Select } from '@vacano/ui'

function App() {
  return (
    <>
      <GlobalStyle />
      <Input label="Имя" placeholder="Введите имя" />
      <Select label="Город" options={cities} onChange={setCity} />
      <Button variant="normal">Отправить</Button>
    </>
  )
}

Четыре точки входа:

  • @vacano/ui — все компоненты

  • @vacano/ui/form — 17 обёрток для react-hook-form (generic, typesafe)

  • @vacano/ui/icons — 1800+ Lucide-иконок

  • @vacano/ui/lib — типы, константы, хуки, 10 Yup-валидаторов

Документация

Библиотека без документации — это исходники на GitHub. Можно разобраться, но зачем? Я потратил на документацию не меньше времени, чем на сами компоненты, и считаю её такой же частью продукта.

Документация для людей

ui.vacano.io — VitePress-сайт, где каждый из 64 компонентов имеет отдельную страницу с одинаковой структурой:

  • Описание — что компонент делает и когда его использовать

  • Таблица пропсов — каждый проп с точным TypeScript-типом ('normal' | 'danger', а не string), значением по умолчанию и описанием

  • Таблица classname-слотов — все доступные слоты для стилизации подэлементов

  • Примеры использования — не минимальные «hello world», а реалистичные сценарии с полными импортами

  • Связанные компоненты — навигация к альтернативам (Select → Autocomplete → MultiSelect)

Помимо компонентов задокументированы утилиты: константы (цвета, брейкпоинтам, z-index'ы), медиа-хелперы (mediaUpmediaDownmediaBetween), хуки, валидация.

Отдельно — Storybook, где можно пощупать каждый компонент в интерактивном режиме, покрутить пропсы и посмотреть как он ведёт себя в разных состояниях.

MCP-сервер — документация для AI-ассистентов

Хорошая документация полезна не только людям. MCP (Model Context Protocol) — стандарт от Anthropic, который позволяет AI-ассистентам подключаться к внешним источникам данных. Vacano UI предоставляет MCP-сервер, через который ассистент получает доступ ко всей документации: все компоненты, все пропсы, ограничения, примеры, рекомендации.

Я осознанно писал документацию с расчётом на два типа читателей: людей и AI-агентов. Это не конфликтующие требования — точные типы, полные примеры и явные ограничения помогают обоим.

Подключение для Claude Code (.mcp.json в корне проекта):

{
  "mcpServers": {
    "vacano-ui": {
      "type": "http",
      "url": "https://tools.vacano.io/ui/mcp"
    }
  }
}

Для Cursor (.cursor/mcp.json):

{
  "mcpServers": {
    "vacano-ui": {
      "command": "npx",
      "args": ["-y", "@anthropic-ai/mcp-remote@latest", "https://tools.vacano.io/ui/mcp"]
    }
  }
}

Для Windsurf — аналогичная настройка через .windsurf/mcp.json.

После подключения ваш AI-ассистент знает все 64 компонента, их пропсы, ограничения, 17 form-обёрток и их generic-типизацию, все точки входа и правильные пути импорта. Вместо того чтобы галлюцинировать несуществующие пропсы, ассистент обращается к MCP-с��рверу и получает актуальную документацию.

Можно сказать: «Сделай форму регистрации с email, паролем, выбором страны и согласием с условиями. Используй Vacano UI.» — и получить рабочий код с правильными импортами, типизацией и всеми нюансами.

Что внутри

64 компонента, разбитых на шесть категорий. Ниже — обзор с акцентом на механику и нетривиальные решения. Полная документация с пропсами, примерами и API — на ui.vacano.io.

Формы

19 базовых компонентов и 17 form-обёрток для react-hook-form.

Базовые — это Input, Select, Autocomplete, DatePicker, Tags, Textarea, Checkbox, Toggle, Radio и их Card/Group-варианты, OtpCode, FileUpload, MultiSelect. Каждый работает самостоятельно с контролируемым и неконтролируемым состоянием.

Form-обёртки убирают бойлерплейт при работе с react-hook-form. Каждая — generic: name типизирован через FieldPath<T>, автокомплитится из типа формы, опечатка в имени поля — ошибка компиляции. Ошибки валидации отображаются автоматически — текст под инпутом, красный вариант у чекбоксов, variant error на всей группе. Не нужно руками доставать ошибки из formState и прокидывать field.value ?? false для булевых контролов — обёртки делают это сами.

FieldRow выравнивает несколько полей в строку через CSS Grid subgrid. Три строки грида: label, input, message. Если у одного поля появляется ошибка — текст занимает третью строку. Соседние поля не сдвигаются: их инпуты остаются на второй строке, третья строка пустая, но ра��мер определяется соседями. Верстка не разъезжается.

10 Yup-валидаторов экспортируются из @vacano/ui/lib: email, password (8+ символов, буква + цифра), phone (международный формат), creditCard (алгоритм Луна, 13-19 цифр), url, slug, ipv4, hexColor, minAge (по дате рождения), noSpaces.

OtpCode — ввод одноразовых кодов. Автопереход между ячейками, вставка из буфера, Backspace с переходом на предыдущую ячейку, навигация стрелками. Под капотом — maxLength={2} вместо 1, потому что на некоторых мобильных клавиатурах при maxLength={1} onChange не срабатывает, если ячейка уже заполнена.

Tags поддерживает режим freeSolo — пользователь может создавать теги, которых нет в списке опций. Tab создаёт тег, Backspace на пустом инпуте удаляет последний. Дропдаун фильтруется в реальном времени и скрывает уже выбранные.

Autocomplete заточен под серверный поиск: принимает async-колбэк onSearchdebounceMs для дебаунса (не нужен lodash), minChars для минимального количества символов перед запросом. Пока идёт запрос — спиннер.

FileUpload — drag & drop с валидацией типа и размера файлов. Если из пяти файлов три подходят, а два нет — принимает три, отклоняет два и вызывает отдельный колбэк onReject с причиной.

Отображение данных

Avatar — не просто картинка в кружочке. Трёхступенчатый fallback: изображение → иконка → инициалы. Инициалы извлекаются умно: из «Яков Саликов» получится «ЯС», из «Admin» — «Ad». Если изображение не загрузилось (ошибка сети, 404) — компонент автоматически переключается на следующий fallback через onErrorAvatarGroup накладывает аватары друг на друга с отступом -25% и показывает «+N» для остальных.

Badge — позиционируемый значок поверх элемента. 4 позиции (top-right, top-left, bottom-right, bottom-left), 2 формы (circle, rectangle), 3 варианта (solid, flat, bordered). Может показывать число, точку или произвольный контент. Автоматически добавляет белый outline, чтобы значок не сливался с фоном.

Card — не просто контейнер с тенью. Три подкомпонента (Header, Body, Footer) для структуры. Режим pressable уменьшает карточку до scale(0.98) при нажатии. hoverable добавляет тень при наведении. blurred включает backdrop-filter: blur(10px) на фоне. footerBlurred — отдельный blur только на футере.

Skeleton поддерживает две анимации: pulse (мерцание opacity 0-1-0) и wave (градиент, бегущий слева направо). Режим circle автоматически делает ширину равной высоте.

Timeline — хронология событий. Каждая запись может содержать title, description, content и actions. Actions — слот для кнопок (редактировать, удалить). Описание всегда под заголовком, actions справа — они не сдвигают текст.

StepLog — пошаговый лог выполнения операций. Каждый шаг имеет статус (success, error, running, pending), длительность и раскрываемые строки лога с нумерацией. Pending-шаги неинтерактивны, остальные — раскрываются кликом.

DateRange отображает диапазон дат с локализацией через Intl.DateTimeFormat. «Январь 2025 — Март 2025» на русском, «January 2025 — March 2025» на английском, «2025年1月 — 2025年3月» на японском. Если конечная дата не указана — показывает настраиваемый текст (по умолчанию «Present Time»).

Обратная связь

Notification и Toastr — две системы уведомлений с разной механикой. Notification — очередь «один за другим». Показывает одно уведомление, после его закрытия (по таймеру или вручную) — следующее. Между показами — 100ms пауза для плавности. Если вызвать show() три раза подряд, пользователь увидит три уведомления последовательно. Toastr — стек «все сразу». Несколько тостов видны одновременно. Если превышен лимит видимых — остальные попадают в очередь и показываются по мере закрытия. Каждый тост имеет собственный таймер (или не имеет — можно сделать toast без автозакрытия). Счётчик «+N» показывает количество уведомлений в очереди. Обе системы работают через Provider-паттерн и хуки.

Confirmation — не компонент, а хук. Вызываешь confirm('Удалить?', async () => { ... }) — появляется диалог с blur-overlay на всю страницу, блокируя интерфейс. Если колбэк возвращает Promise — кнопка «Подтвердить» показывает спиннер, «Отмена» блокируется до завершения. Закрытие по Escape и клику на overlay. Анимации входа и выхода — 200ms.

Modal рендерится через портал в document.body. Overlay с полупрозрачным фоном, контент по центру с max-height: calc(100vh - 32px) и прокруткой. Drawer — то же, но выезжает с одной из четырёх сторон (left, right, top, bottom) с настраиваемым размером.

PendingScreen — экран ожидания для долгих операций. Вместо спиннера показывает анимированные фразы в стиле split-flap табло аэропорта. Каждая буква независимо перебирает случайные символы, затем «защёлкивается» на правильной — волной слева направо. 78 встроенных фраз в случайном порядке без повторов (Fisher-Yates shuffle). Полный цикл — около 4.5 минут. Можно передать свои фразы и настроить интервал.

Tooltip позиционируется с учётом viewport: если сверху не хватает места — покажется снизу. Координаты зажимаются к краям экрана с отступом 8px, чтобы тултип не обрезался.

Навигация

Accordion раскрывается через CSS Grid 0fr → 1fr с transition — никакого max-height: 9999px. Работает с контентом любой высоты. Два визуальных варианта: outlined (разделители между секциями) и splitted (отдельные карточки с gap). Режим multiple позволяет держать открытыми несколько секций. Контролируемое и неконтролируемое состояние.

Pagination вычисляет диапазон видимых страниц через алгоритм с параметрами siblings (страницы вокруг текущей) и boundaries (страницы на краях). Между ними — ellipsis. Анимированный курсор плавно скользит к активной странице через transform: translateX(). Режим loop — после последней страницы следующая кнопка ведёт на первую.

Breadcrumbs коллапсируют длинные цепочки: настраиваемое количество элементов в начале и конце, между ними — ellipsis. Разделитель кастомизируется.

Stepper — пошаговый прогресс. Три визуальных состояния: active, completed, pending. Горизонтальная и вертикальная ориентация. Линии между шагами меняют цвет по завершению. Опциональная навигация кликом.

MenuButton — анимированный гамбургер. Три полоски плавно трансформируются в крестик: верхняя поворачивается на -45°, средняя исчезает (opacity 0), нижняя поворачивается на +45°.

Лейаут

Container — responsive обёртка с max-width по брейкпоинтам (sm: 640, md: 768, lg: 1024, xl: 1280, 2xl: 1536). Divider — горизонтальная линия с опциональным лейблом по центру (два отрезка и текст между ними). Panel — контейнер с dashed-бордером, лейблом, заголовком и описанием; два варианта: light и dark. ShellScreen — полноэкранный шаблон с декоративной фоновой сеткой, анимированными кольцами вокруг иконки, секциями для лого, заголовка и контента.

Утилиты

FieldLabel и FieldMessage — строительные блоки для кастомных полей. FieldLabel рендерит лейбл с опциональной звёздочкой * для required. FieldMessage — текст под полем с цветом по варианту: серый (normal), красный (error), зелёный (success), жёлтый (warning). Оба возвращают null если нет контента — не нужен условный рендеринг снаружи.

ImageCropper — React-обёртка вокруг моей же framework-agnostic библиотеки hq-cropper. hq-cropper не привязан к фреймворку — работает с ванильным JS, Vue, Svelte, чем угодно. В Vacano UI он обёрнут в хук useImageCropper с lazy-инициализацией: библиотека кропа загружается только при первом открытии, а не при монтировании. Возвращает и base64, и blob. Настраиваемый размер выхода, степень сжатия и лимит на размер файла.

KeysBindings и KeySymbol — отображение горячих клавиш. Платформозависимые символы: Meta →  на Mac, Win на Windows. Control →  на Mac, Ctrl на Windows. И так для всех модификаторов. Хук useKeyBinding отслеживает комбинации клавиш через Set нажатых ключей — срабатывает только когда все клавиши в комбинации зажаты одновременно.

SplitFlapText — анимация «табло аэропорта». Можно использовать отдельно от PendingScreen — например, для бегущих котировок, статусов или таймеров.

Сквозные паттерны

Все 64 компонента разделяют общие принципы:

  • Два размера — compact и default

  • Варианты — normalsuccesswarningdanger и другие

  • Classname-слоты — типизированный classnames для стилизации подэлементов

  • data-test-id — единый проп для автотестов

  • ref-проброс — React 19 style через пропсы

  • Порталы — 6 компонентов с portalRenderNode для рендеринга дропдаунов поверх overflow: hidden

  • Z-index'ы — общая константа Z_INDEX для всех слоёв: dropdown (100) → modalOverlay (1000) → modal (1001) → portalDropdown (1002) → confirmation (1003)

  • Keyboard — Escape для закрытия, стрелки для навигации, Enter/Space для выбора

  • Горячие клавиши — Button принимает keyBindings={['Meta', 'S']} с платформозависимыми символами

  • Локализация — DatePicker и DateRange через Intl.DateTimeFormat, 400+ локалей без зависимостей

  • 1800+ иконок — обёртки над Lucide Icons, tree-shaking уберёт неиспользуемые

Ссылки

От автора:
Статью валидировал и правил в Claude Code. В виду СДВГ не мастер писать лонгриды, но я пострался показать в этой статье труд 3 лет, пусть и через ИИ. Если библиотека показалась полезной — поставьте звёздочку на GitHub, это помогает проекту расти, но особенно рад буду если попробуете ее в своих проектах. Почти все решениея здесь выросли из труда сотен людей - от QA до аналитиков (и доолгих созвонов с ними на тему "как эта штука все таки должна работать")

Буду рад вопросам и фидбэку в комментариях.

Тхн= <3