Hi! I'm Iakov, UI Kit Lead at Exante. At work, we maintain a proprietary design system -- powerful, tailored to our specific needs, but closed-source. In my spare time, I rethought a number of solutions from my day job, reworked them into general-purpose patterns, and packaged them as an open-source library -- Vacano UI.
64 components, 17 form wrappers, 1800+ icons, 10 validators, documentation for humans, and an MCP server for AI assistants.
Philosophy
There are plenty of UI libraries. Why another one?
Most UI kits fall into two camps. Headless libraries give you logic without styles -- powerful and flexible, but you're on your own for the visuals. Opinionated libraries give you polished components out of the box, but lock you into a rigid design system that's hard to escape.
Vacano UI sits in between. Components ship with a ready-made look and work out of the box. But every sub-element is accessible for styling through typed classname slots -- no !important, no nested selectors, no digging through the DOM inspector. Want to change the trigger color on a Select? Pass classnames={{ trigger: 'my-trigger' }} and write plain CSS. TypeScript tells you which slots are available for each component.
Second principle -- minimal ceremony to get started. No global ThemeProvider, no createTheme, no token config that everything depends on. Install the package, import a component, render it. Providers are only required for the components that genuinely need them: Confirmation, Notification, Toastr, SaveProgress, NotifyConfirmation -- they use context and hooks because they manage global state. The other 59 components are fully autonomous.
Third principle -- every component is finished, not half-baked. Not "here's a <select> with some classes, good luck," but a complete solution with all the edge cases that surface in production. Dropdown clipped inside a modal? Portal. Date localization? Intl.DateTimeFormat, zero dependencies. OTP input on a mobile keyboard? maxLength hack. Validation error breaks the layout of adjacent fields? CSS Grid subgrid. Details on each component below.
Getting Started
pnpm add @vacano/ui @emotion/react @emotion/styled
import { GlobalStyle, Button, Input, Select } from '@vacano/ui'
function App() {
return (
<>
<GlobalStyle />
<Input label="Name" placeholder="Enter name" />
<Select label="City" options={cities} onChange={setCity} />
<Button variant="normal">Submit</Button>
</>
)
}
Four entry points:
-
@vacano/ui-- all components -
@vacano/ui/form-- 17 react-hook-form wrappers (generic, typesafe) -
@vacano/ui/icons-- 1800+ Lucide icons -
@vacano/ui/lib-- types, constants, hooks, 10 Yup validators
Documentation
A library without documentation is just source code on GitHub. You can figure it out, but why should you have to? I spent no less time on the docs than on the components themselves, and I consider documentation an equal part of the product.
Documentation for Humans
ui.vacano.io -- a VitePress site where each of the 64 components has its own page with a consistent structure:
- Description -- what the component does and when to use it
-
Props table -- every prop with its exact TypeScript type (
'normal' | 'danger', not juststring), default value, and description - Classname slots table -- all available slots for sub-element styling
- Usage examples -- not minimal "hello world" snippets, but realistic scenarios with full imports
- Related components -- navigation to alternatives (Select → Autocomplete → MultiSelect)
Beyond components, the docs cover utilities: constants (colors, breakpoints, z-indexes), media helpers (mediaUp, mediaDown, mediaBetween), hooks, and validation.
There's also a Storybook where you can interact with every component, tweak props, and see how it behaves in different states.
MCP Server -- Documentation for AI Assistants
Good documentation isn't just useful for humans. MCP (Model Context Protocol) is a standard from Anthropic that lets AI assistants connect to external data sources. Vacano UI provides an MCP server that gives the assistant access to the entire documentation: all components, all props, constraints, examples, and recommendations.
I deliberately wrote the docs for two types of readers: humans and AI agents. These aren't conflicting requirements -- precise types, complete examples, and explicit constraints help both.
Setup for Claude Code (.mcp.json in your project root):
{
"mcpServers": {
"vacano-ui": {
"type": "http",
"url": "https://tools.vacano.io/ui/mcp"
}
}
}
For Cursor (.cursor/mcp.json):
{
"mcpServers": {
"vacano-ui": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-remote@latest", "https://tools.vacano.io/ui/mcp"]
}
}
}
For Windsurf -- similar setup via .windsurf/mcp.json.
Once connected, your AI assistant knows all 64 components, their props, constraints, the 17 form wrappers and their generic typing, all entry points and correct import paths. Instead of hallucinating non-existent props, the assistant queries the MCP server and gets up-to-date documentation.
You can say: "Build a registration form with email, password, country selection, and terms acceptance. Use Vacano UI." -- and get working code with correct imports, typing, and all the nuances.
What's Inside
64 components split into six categories. Below is an overview focused on mechanics and non-trivial solutions. Full documentation with props, examples, and API is at ui.vacano.io.
Forms
19 base components and 17 form wrappers for react-hook-form.
The base components are Input, Select, Autocomplete, DatePicker, Tags, Textarea, Checkbox, Toggle, Radio and their Card/Group variants, OtpCode, FileUpload, MultiSelect. Each works standalone with controlled and uncontrolled state.
Form wrappers eliminate react-hook-form boilerplate. Each one is generic: name is typed via FieldPath<T>, autocompletes from the form type, a typo in the field name is a compile error. Validation errors display automatically -- error text under text inputs, red variant on checkboxes, variant error on entire groups. No need to manually extract errors from formState or pass field.value ?? false for boolean controls -- the wrappers handle it.
FieldRow aligns multiple fields in a row via CSS Grid subgrid. Three grid rows: label, input, message. When one field shows a validation error, the text occupies the third row. Adjacent fields don't shift: their inputs stay on the second row, the third row is empty but its size is determined by neighbors. Layout stays intact.
10 Yup validators are exported from @vacano/ui/lib: email, password (8+ chars, letter + digit), phone (international format), creditCard (Luhn algorithm, 13-19 digits), url, slug, ipv4, hexColor, minAge (by birth date), noSpaces.
OtpCode -- one-time code input. Auto-advance between cells, clipboard paste support, Backspace navigates to previous cell, arrow key navigation. Under the hood -- maxLength={2} instead of 1, because on some mobile keyboards onChange doesn't fire with maxLength={1} when the cell is already filled.
Tags supports freeSolo mode -- users can create tags not in the options list. Tab creates a tag, Backspace on an empty input removes the last one. The dropdown filters in real time and hides already selected tags.
Autocomplete is built for server-side search: accepts an async onSearch callback, debounceMs for debouncing (no lodash needed), minChars for minimum characters before querying. Shows a spinner while loading.
FileUpload -- drag & drop with type and size validation. If three out of five files pass and two don't -- it accepts the three, rejects the two, and fires a separate onReject callback with the reason.
Data Display
Avatar -- not just a picture in a circle. Three-tier fallback: image → icon → initials. Initials are extracted smartly: "Yakov Salikov" becomes "YS", "Admin" becomes "Ad". If the image fails to load (network error, 404), the component automatically switches to the next fallback via onError. AvatarGroup overlaps avatars with a -25% offset and shows "+N" for the rest.
Badge -- a positioned indicator on top of an element. 4 placements (top-right, top-left, bottom-right, bottom-left), 2 shapes (circle, rectangle), 3 variants (solid, flat, bordered). Can display a number, a dot, or arbitrary content. Automatically adds a white outline so the badge doesn't blend with the background.
Card -- not just a container with a shadow. Three sub-components (Header, Body, Footer) for structure. pressable mode scales the card to scale(0.98) on click. hoverable adds shadow on hover. blurred enables backdrop-filter: blur(10px) on the background. footerBlurred -- separate blur on just the footer.
Skeleton supports two animations: pulse (opacity 0-1-0 flicker) and wave (gradient sweeping left to right). circle mode automatically makes width equal to height.
Timeline -- event chronology. Each entry can have title, description, content, and actions. Actions is a slot for buttons (edit, delete). Description always sits under the title, actions float right -- they don't push the text down.
StepLog -- step-by-step execution log. Each step has a status (success, error, running, pending), duration, and expandable log lines with line numbers. Pending steps are non-interactive, others expand on click.
DateRange displays a date range with localization via Intl.DateTimeFormat. "January 2025 — March 2025" in English, "Январь 2025 — Март 2025" in Russian, "2025年1月 — 2025年3月" in Japanese. If no end date is provided, it shows configurable text ("Present Time" by default).
Feedback
Notification and Toastr -- two notification systems with different mechanics. Notification is a "one at a time" queue. It shows one notification; after it closes (by timer or manually), the next one appears. A 100ms pause between shows for smoothness. Call show() three times in a row and the user sees three notifications sequentially. Toastr is a "show all" stack. Multiple toasts are visible simultaneously. If the visible limit is exceeded, the rest queue up and appear as others close. Each toast has its own timer (or none -- you can make a toast without auto-dismiss). A "+N" counter shows the number of queued notifications. Both systems work through the Provider pattern and hooks.
Confirmation -- not a component, but a hook. Call confirm('Delete?', async () => { ... }) and a dialog appears with a blur overlay covering the entire page, blocking the interface. If the callback returns a Promise, the "Confirm" button shows a spinner and "Cancel" is disabled until completion. Dismissable via Escape and overlay click. Enter and exit animations -- 200ms.
Modal renders via portal to document.body. Overlay with semi-transparent background, content centered with max-height: calc(100vh - 32px) and scroll. Drawer is the same idea but slides in from one of four sides (left, right, top, bottom) with a configurable size.
PendingScreen -- a waiting screen for long-running operations. Instead of a spinner, it shows animated phrases in the style of a split-flap airport departure board. Each letter independently cycles through random characters, then "snaps" to the correct one -- in a wave from left to right. 78 built-in phrases in random order without repeats (Fisher-Yates shuffle). Full cycle is about 4.5 minutes. You can pass custom phrases and adjust the interval.
Tooltip positions itself with viewport awareness: if there's not enough space above, it shows below. Coordinates are clamped to screen edges with 8px padding to prevent clipping.
Navigation
Accordion expands via CSS Grid 0fr → 1fr with transition -- no max-height: 9999px. Works with any content height. Two visual variants: outlined (dividers between sections) and splitted (separate cards with a gap). multiple mode allows keeping several sections open. Controlled and uncontrolled state.
Pagination calculates the visible page range via an algorithm with siblings (pages around current) and boundaries (pages at edges) parameters. Ellipsis between them. An animated cursor smoothly slides to the active page via transform: translateX(). loop mode -- after the last page, the next button goes to the first.
Breadcrumbs collapse long chains: configurable number of items at the start and end, ellipsis in between. Separator is customizable.
Stepper -- step-by-step progress. Three visual states: active, completed, pending. Horizontal and vertical orientation. Lines between steps change color on completion. Optional click-to-navigate.
MenuButton -- an animated hamburger. Three bars smoothly transform into a cross: top rotates -45°, middle fades out (opacity 0), bottom rotates +45°.
Layout
Container -- responsive wrapper with max-width by breakpoints (sm: 640, md: 768, lg: 1024, xl: 1280, 2xl: 1536). Divider -- a horizontal line with an optional centered label (two line segments with text between them). Panel -- a container with a dashed border, label, title, and description; two variants: light and dark. ShellScreen -- a full-screen template with a decorative background grid, animated rings around an icon, and sections for logo, header, and content.
Utilities
FieldLabel and FieldMessage -- building blocks for custom fields. FieldLabel renders a label with an optional asterisk * for required. FieldMessage -- text below the field colored by variant: gray (normal), red (error), green (success), yellow (warning). Both return null if there's no content -- no conditional rendering needed on the outside.
ImageCropper -- a React wrapper around my own framework-agnostic library hq-cropper. hq-cropper isn't tied to any framework -- it works with vanilla JS, Vue, Svelte, anything. In Vacano UI it's wrapped in a useImageCropper hook with lazy initialization: the cropping library only loads on first open, not on mount. Returns both base64 and blob. Configurable output size, compression, and file size limit.
KeysBindings and KeySymbol -- keyboard shortcut display. Platform-aware symbols: Meta → ⌘ on Mac, Win on Windows. Control → ⌃ on Mac, Ctrl on Windows. And so on for all modifiers. The useKeyBinding hook tracks key combinations via a Set of pressed keys -- fires only when all keys in the combination are held simultaneously.
SplitFlapText -- the "airport departure board" animation. Can be used independently from PendingScreen -- for example, for stock tickers, status displays, or timers.
Cross-Cutting Patterns
All 64 components share common principles:
-
Two sizes --
compactanddefault -
Variants --
normal,success,warning,danger, and others -
Classname slots -- typed
classnamesfor sub-element styling - data-test-id -- a single prop for test automation
- ref forwarding -- React 19 style via props
-
Portals -- 6 components with
portalRenderNodefor rendering dropdowns aboveoverflow: hidden -
Z-indexes -- shared
Z_INDEXconstant for all layers: dropdown (100) → modalOverlay (1000) → modal (1001) → portalDropdown (1002) → confirmation (1003) - Keyboard -- Escape to close, arrows to navigate, Enter/Space to select
-
Hotkeys -- Button accepts
keyBindings={['Meta', 'S']}with platform-aware symbols -
Localization -- DatePicker and DateRange via
Intl.DateTimeFormat, 400+ locales with zero dependencies - 1800+ icons -- Lucide Icons wrappers, tree-shaking removes unused ones
Links
- Documentation: ui.vacano.io
- Storybook: ui.vacano.io/storybook
- GitHub: github.com/vacano-house/vacano-ui
- npm: @vacano/ui
- License: MIT
If you find the library useful, a star on GitHub helps the project grow. Happy to answer questions and hear feedback in the comments.
Top comments (4)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.