Привет, Хабр! Меня зовут Яков, я 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'ы), медиа-хелперы (mediaUp, mediaDown, mediaBetween), хуки, валидация.
Отдельно — 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-колбэк onSearch, debounceMs для дебаунса (не нужен lodash), minChars для минимального количества символов перед запросом. Пока идёт запрос — спиннер.
FileUpload — drag & drop с валидацией типа и размера файлов. Если из пяти файлов три подходят, а два нет — принимает три, отклоняет два и вызывает отдельный колбэк onReject с причиной.
Отображение данных
Avatar — не просто картинка в кружочке. Трёхступенчатый fallback: изображение → иконка → инициалы. Инициалы извлекаются умно: из «Яков Саликов» получится «ЯС», из «Admin» — «Ad». Если изображение не загрузилось (ошибка сети, 404) — компонент автоматически переключается на следующий fallback через onError. AvatarGroup накладывает аватары друг на друга с отступом -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Варианты —
normal,success,warning,dangerи другиеClassname-слоты — типизированный
classnamesдля стилизации подэлементовdata-test-id — единый проп для автотестов
ref-проброс — React 19 style через пропсы
Порталы — 6 компонентов с
portalRenderNodeдля рендеринга дропдаунов поверхoverflow: hiddenZ-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 уберёт неиспользуемые
Ссылки
Документация: ui.vacano.io
Storybook: ui.vacano.io/storybook
npm: @vacano/ui
Лицензия: MIT
От автора:
Статью валидировал и правил в Claude Code. В виду СДВГ не мастер писать лонгриды, но я пострался показать в этой статье труд 3 лет, пусть и через ИИ. Если библиотека показалась полезной — поставьте звёздочку на GitHub, это помогает проекту расти, но особенно рад буду если попробуете ее в своих проектах. Почти все решениея здесь выросли из труда сотен людей - от QA до аналитиков (и доолгих созвонов с ними на тему "как эта штука все таки должна работать")
Буду рад вопросам и фидбэку в комментариях.
Тхн= <3
