<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Iakov Salikov</title>
    <description>The latest articles on DEV Community by Iakov Salikov (@isalikov).</description>
    <link>https://dev.to/isalikov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1362245%2F3dee48a6-3961-4704-a521-79b9139657b0.jpeg</url>
      <title>DEV Community: Iakov Salikov</title>
      <link>https://dev.to/isalikov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isalikov"/>
    <language>en</language>
    <item>
      <title>Vacano UI - 64 React Components with an MCP Server for AI Assistants</title>
      <dc:creator>Iakov Salikov</dc:creator>
      <pubDate>Tue, 24 Feb 2026 18:06:05 +0000</pubDate>
      <link>https://dev.to/isalikov/vacano-ui-64-react-components-with-an-mcp-server-for-ai-assistants-1d6j</link>
      <guid>https://dev.to/isalikov/vacano-ui-64-react-components-with-an-mcp-server-for-ai-assistants-1d6j</guid>
      <description>&lt;p&gt;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 -- &lt;a href="https://github.com/vacano-house/vacano-ui" rel="noopener noreferrer"&gt;Vacano UI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;64 components, 17 form wrappers, 1800+ icons, 10 validators, documentation for humans, and an MCP server for AI assistants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Philosophy
&lt;/h2&gt;

&lt;p&gt;There are plenty of UI libraries. Why another one?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;!important&lt;/code&gt;, no nested selectors, no digging through the DOM inspector. Want to change the trigger color on a Select? Pass &lt;code&gt;classnames={{ trigger: 'my-trigger' }}&lt;/code&gt; and write plain CSS. TypeScript tells you which slots are available for each component.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Third principle -- every component is finished, not half-baked. Not "here's a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; 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? &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;, zero dependencies. OTP input on a mobile keyboard? &lt;code&gt;maxLength&lt;/code&gt; hack. Validation error breaks the layout of adjacent fields? CSS Grid subgrid. Details on each component below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @vacano/ui @emotion/react @emotion/styled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GlobalStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Select&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vacano/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GlobalStyle&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Enter name"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Select&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"City"&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cities&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setCity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"normal"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@vacano/ui&lt;/code&gt; -- all components&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@vacano/ui/form&lt;/code&gt; -- 17 react-hook-form wrappers (generic, typesafe)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@vacano/ui/icons&lt;/code&gt; -- 1800+ Lucide icons&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@vacano/ui/lib&lt;/code&gt; -- types, constants, hooks, 10 Yup validators&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Documentation
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Documentation for Humans
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://ui.vacano.io/" rel="noopener noreferrer"&gt;ui.vacano.io&lt;/a&gt; -- a VitePress site where each of the 64 components has its own page with a consistent structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt; -- what the component does and when to use it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Props table&lt;/strong&gt; -- every prop with its exact TypeScript type (&lt;code&gt;'normal' | 'danger'&lt;/code&gt;, not just &lt;code&gt;string&lt;/code&gt;), default value, and description&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classname slots table&lt;/strong&gt; -- all available slots for sub-element styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage examples&lt;/strong&gt; -- not minimal "hello world" snippets, but realistic scenarios with full imports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Related components&lt;/strong&gt; -- navigation to alternatives (Select → Autocomplete → MultiSelect)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond components, the docs cover utilities: constants (colors, breakpoints, z-indexes), media helpers (&lt;code&gt;mediaUp&lt;/code&gt;, &lt;code&gt;mediaDown&lt;/code&gt;, &lt;code&gt;mediaBetween&lt;/code&gt;), hooks, and validation.&lt;/p&gt;

&lt;p&gt;There's also a &lt;a href="https://ui.vacano.io/storybook/?path=/story/components-accordion--playground" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt; where you can interact with every component, tweak props, and see how it behaves in different states.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Server -- Documentation for AI Assistants
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Setup for Claude Code (&lt;code&gt;.mcp.json&lt;/code&gt; in your project root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vacano-ui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://tools.vacano.io/ui/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cursor (&lt;code&gt;.cursor/mcp.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vacano-ui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@anthropic-ai/mcp-remote@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://tools.vacano.io/ui/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Windsurf -- similar setup via &lt;code&gt;.windsurf/mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Inside
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://ui.vacano.io/" rel="noopener noreferrer"&gt;ui.vacano.io&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forms
&lt;/h3&gt;

&lt;p&gt;19 base components and 17 form wrappers for react-hook-form.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Form wrappers eliminate react-hook-form boilerplate. Each one is generic: &lt;code&gt;name&lt;/code&gt; is typed via &lt;code&gt;FieldPath&amp;lt;T&amp;gt;&lt;/code&gt;, 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 &lt;code&gt;formState&lt;/code&gt; or pass &lt;code&gt;field.value ?? false&lt;/code&gt; for boolean controls -- the wrappers handle it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FieldRow&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;10 Yup validators are exported from &lt;code&gt;@vacano/ui/lib&lt;/code&gt;: email, password (8+ chars, letter + digit), phone (international format), creditCard (Luhn algorithm, 13-19 digits), url, slug, ipv4, hexColor, minAge (by birth date), noSpaces.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OtpCode&lt;/code&gt; -- one-time code input. Auto-advance between cells, clipboard paste support, Backspace navigates to previous cell, arrow key navigation. Under the hood -- &lt;code&gt;maxLength={2}&lt;/code&gt; instead of 1, because on some mobile keyboards &lt;code&gt;onChange&lt;/code&gt; doesn't fire with &lt;code&gt;maxLength={1}&lt;/code&gt; when the cell is already filled.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Tags&lt;/code&gt; supports &lt;code&gt;freeSolo&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Autocomplete&lt;/code&gt; is built for server-side search: accepts an async &lt;code&gt;onSearch&lt;/code&gt; callback, &lt;code&gt;debounceMs&lt;/code&gt; for debouncing (no lodash needed), &lt;code&gt;minChars&lt;/code&gt; for minimum characters before querying. Shows a spinner while loading.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FileUpload&lt;/code&gt; -- drag &amp;amp; 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 &lt;code&gt;onReject&lt;/code&gt; callback with the reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Display
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Avatar&lt;/code&gt; -- 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 &lt;code&gt;onError&lt;/code&gt;. &lt;code&gt;AvatarGroup&lt;/code&gt; overlaps avatars with a -25% offset and shows "+N" for the rest.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Badge&lt;/code&gt; -- 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Card&lt;/code&gt; -- not just a container with a shadow. Three sub-components (Header, Body, Footer) for structure. &lt;code&gt;pressable&lt;/code&gt; mode scales the card to &lt;code&gt;scale(0.98)&lt;/code&gt; on click. &lt;code&gt;hoverable&lt;/code&gt; adds shadow on hover. &lt;code&gt;blurred&lt;/code&gt; enables &lt;code&gt;backdrop-filter: blur(10px)&lt;/code&gt; on the background. &lt;code&gt;footerBlurred&lt;/code&gt; -- separate blur on just the footer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Skeleton&lt;/code&gt; supports two animations: &lt;code&gt;pulse&lt;/code&gt; (opacity 0-1-0 flicker) and &lt;code&gt;wave&lt;/code&gt; (gradient sweeping left to right). &lt;code&gt;circle&lt;/code&gt; mode automatically makes width equal to height.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Timeline&lt;/code&gt; -- 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;StepLog&lt;/code&gt; -- 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DateRange&lt;/code&gt; displays a date range with localization via &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;. "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).&lt;/p&gt;

&lt;h3&gt;
  
  
  Feedback
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Notification&lt;/code&gt; and &lt;code&gt;Toastr&lt;/code&gt; -- 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 &lt;code&gt;show()&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Confirmation&lt;/code&gt; -- not a component, but a hook. Call &lt;code&gt;confirm('Delete?', async () =&amp;gt; { ... })&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Modal&lt;/code&gt; renders via portal to &lt;code&gt;document.body&lt;/code&gt;. Overlay with semi-transparent background, content centered with &lt;code&gt;max-height: calc(100vh - 32px)&lt;/code&gt; and scroll. &lt;code&gt;Drawer&lt;/code&gt; is the same idea but slides in from one of four sides (left, right, top, bottom) with a configurable size.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PendingScreen&lt;/code&gt; -- 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Tooltip&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Accordion&lt;/code&gt; expands via CSS Grid &lt;code&gt;0fr → 1fr&lt;/code&gt; with transition -- no &lt;code&gt;max-height: 9999px&lt;/code&gt;. Works with any content height. Two visual variants: &lt;code&gt;outlined&lt;/code&gt; (dividers between sections) and &lt;code&gt;splitted&lt;/code&gt; (separate cards with a gap). &lt;code&gt;multiple&lt;/code&gt; mode allows keeping several sections open. Controlled and uncontrolled state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Pagination&lt;/code&gt; calculates the visible page range via an algorithm with &lt;code&gt;siblings&lt;/code&gt; (pages around current) and &lt;code&gt;boundaries&lt;/code&gt; (pages at edges) parameters. Ellipsis between them. An animated cursor smoothly slides to the active page via &lt;code&gt;transform: translateX()&lt;/code&gt;. &lt;code&gt;loop&lt;/code&gt; mode -- after the last page, the next button goes to the first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Breadcrumbs&lt;/code&gt; collapse long chains: configurable number of items at the start and end, ellipsis in between. Separator is customizable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Stepper&lt;/code&gt; -- 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MenuButton&lt;/code&gt; -- an animated hamburger. Three bars smoothly transform into a cross: top rotates -45°, middle fades out (opacity 0), bottom rotates +45°.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layout
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Container&lt;/code&gt; -- responsive wrapper with max-width by breakpoints (sm: 640, md: 768, lg: 1024, xl: 1280, 2xl: 1536). &lt;code&gt;Divider&lt;/code&gt; -- a horizontal line with an optional centered label (two line segments with text between them). &lt;code&gt;Panel&lt;/code&gt; -- a container with a dashed border, label, title, and description; two variants: light and dark. &lt;code&gt;ShellScreen&lt;/code&gt; -- a full-screen template with a decorative background grid, animated rings around an icon, and sections for logo, header, and content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Utilities
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FieldLabel&lt;/code&gt; and &lt;code&gt;FieldMessage&lt;/code&gt; -- building blocks for custom fields. FieldLabel renders a label with an optional asterisk &lt;code&gt;*&lt;/code&gt; for required. FieldMessage -- text below the field colored by variant: gray (normal), red (error), green (success), yellow (warning). Both return &lt;code&gt;null&lt;/code&gt; if there's no content -- no conditional rendering needed on the outside.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ImageCropper&lt;/code&gt; -- a React wrapper around my own framework-agnostic library &lt;a href="https://github.com/isalikov/hq-cropper" rel="noopener noreferrer"&gt;hq-cropper&lt;/a&gt;. hq-cropper isn't tied to any framework -- it works with vanilla JS, Vue, Svelte, anything. In Vacano UI it's wrapped in a &lt;code&gt;useImageCropper&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KeysBindings&lt;/code&gt; and &lt;code&gt;KeySymbol&lt;/code&gt; -- keyboard shortcut display. Platform-aware symbols: Meta → &lt;code&gt;⌘&lt;/code&gt; on Mac, &lt;code&gt;Win&lt;/code&gt; on Windows. Control → &lt;code&gt;⌃&lt;/code&gt; on Mac, &lt;code&gt;Ctrl&lt;/code&gt; on Windows. And so on for all modifiers. The &lt;code&gt;useKeyBinding&lt;/code&gt; hook tracks key combinations via a Set of pressed keys -- fires only when all keys in the combination are held simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SplitFlapText&lt;/code&gt; -- the "airport departure board" animation. Can be used independently from PendingScreen -- for example, for stock tickers, status displays, or timers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Cutting Patterns
&lt;/h3&gt;

&lt;p&gt;All 64 components share common principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two sizes&lt;/strong&gt; -- &lt;code&gt;compact&lt;/code&gt; and &lt;code&gt;default&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants&lt;/strong&gt; -- &lt;code&gt;normal&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;danger&lt;/code&gt;, and others&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classname slots&lt;/strong&gt; -- typed &lt;code&gt;classnames&lt;/code&gt; for sub-element styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;data-test-id&lt;/strong&gt; -- a single prop for test automation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ref forwarding&lt;/strong&gt; -- React 19 style via props&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portals&lt;/strong&gt; -- 6 components with &lt;code&gt;portalRenderNode&lt;/code&gt; for rendering dropdowns above &lt;code&gt;overflow: hidden&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Z-indexes&lt;/strong&gt; -- shared &lt;code&gt;Z_INDEX&lt;/code&gt; constant for all layers: dropdown (100) → modalOverlay (1000) → modal (1001) → portalDropdown (1002) → confirmation (1003)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard&lt;/strong&gt; -- Escape to close, arrows to navigate, Enter/Space to select&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hotkeys&lt;/strong&gt; -- Button accepts &lt;code&gt;keyBindings={['Meta', 'S']}&lt;/code&gt; with platform-aware symbols&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localization&lt;/strong&gt; -- DatePicker and DateRange via &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt;, 400+ locales with zero dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1800+ icons&lt;/strong&gt; -- Lucide Icons wrappers, tree-shaking removes unused ones&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Documentation: &lt;a href="https://ui.vacano.io/" rel="noopener noreferrer"&gt;ui.vacano.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Storybook: &lt;a href="https://ui.vacano.io/storybook/?path=/story/components-accordion--playground" rel="noopener noreferrer"&gt;ui.vacano.io/storybook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/vacano-house/vacano-ui" rel="noopener noreferrer"&gt;github.com/vacano-house/vacano-ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@vacano/ui" rel="noopener noreferrer"&gt;@vacano/ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find the library useful, a star on &lt;a href="https://github.com/vacano-house/vacano-ui" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps the project grow. Happy to answer questions and hear feedback in the comments.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>react</category>
      <category>showdev</category>
      <category>ui</category>
    </item>
    <item>
      <title>hq-cropper: Zero-Dependency Image Cropper for JS</title>
      <dc:creator>Iakov Salikov</dc:creator>
      <pubDate>Sat, 13 Dec 2025 12:43:24 +0000</pubDate>
      <link>https://dev.to/isalikov/hq-cropper-zero-dependency-image-cropper-for-js-mo0</link>
      <guid>https://dev.to/isalikov/hq-cropper-zero-dependency-image-cropper-for-js-mo0</guid>
      <description>&lt;p&gt;hq-cropper: Zero-Dependency Image Cropper for JS&lt;/p&gt;

&lt;p&gt;Have you ever needed a simple, lightweight image cropper for profile pictures or avatars? I've been working on &lt;strong&gt;hq-cropper&lt;/strong&gt; — a zero-dependency TypeScript library that does exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Large Images
&lt;/h2&gt;

&lt;p&gt;Here's a common scenario: your user uploads a 4000×3000 pixel photo from their smartphone, but you only need a 200×200 avatar. Most croppers handle this poorly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Naive approach&lt;/strong&gt;: Crop at full resolution, then resize → wastes memory, slow on mobile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple resize&lt;/strong&gt;: Downscale first, then crop → loses too much quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed output size&lt;/strong&gt;: Always outputs the same dimensions → no flexibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge is finding the right balance: you want small output files, but you don't want to destroy quality when the source image is already small.&lt;/p&gt;

&lt;h2&gt;
  
  
  How hq-cropper Solves This
&lt;/h2&gt;

&lt;p&gt;hq-cropper uses a &lt;strong&gt;logarithmic scaling algorithm&lt;/strong&gt; controlled by the &lt;code&gt;quality&lt;/code&gt; parameter. Here's the key insight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small source images&lt;/strong&gt; → minimal or no downscaling (preserves quality)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large source images&lt;/strong&gt; → proportional downscaling (reduces file size)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;quality&lt;/code&gt; parameter (default: &lt;code&gt;1.01&lt;/code&gt;) controls this behavior. It's the logarithm base used to calculate output dimensions from the crop selection size.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;outputSize = log(cropSelectionSize) / log(quality)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;quality: 1.01&lt;/code&gt; → Large output (almost 1:1 with selection)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;quality: 1.5&lt;/code&gt; → Medium output (good balance)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;quality: 2.0&lt;/code&gt; → Small output (aggressive compression)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Avatar Upload (Balance Quality &amp;amp; Size)
&lt;/h3&gt;

&lt;p&gt;For profile pictures where you want decent quality but reasonable file sizes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HqCropper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: A 500px crop selection produces ~180px output. A 200px selection produces ~150px output. Small selections stay sharp, large selections get reasonably compressed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thumbnail Generation (Smallest Possible)
&lt;/h3&gt;

&lt;p&gt;When file size matters most (e.g., gallery thumbnails):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HqCropper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Aggressive downscaling. A 500px selection → ~130px output. Perfect for thumbnails where you need tiny files.&lt;/p&gt;

&lt;h3&gt;
  
  
  High-Quality Crop (Preserve Details)
&lt;/h3&gt;

&lt;p&gt;When quality is paramount (e.g., portfolio images):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HqCropper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Nearly 1:1 output. A 500px selection → ~490px output. Maximum quality, larger files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source Image&lt;/th&gt;
&lt;th&gt;Crop Selection&lt;/th&gt;
&lt;th&gt;quality: 1.01&lt;/th&gt;
&lt;th&gt;quality: 1.5&lt;/th&gt;
&lt;th&gt;quality: 2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4000×3000&lt;/td&gt;
&lt;td&gt;800px&lt;/td&gt;
&lt;td&gt;~780px&lt;/td&gt;
&lt;td&gt;~210px&lt;/td&gt;
&lt;td&gt;~130px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1200×800&lt;/td&gt;
&lt;td&gt;400px&lt;/td&gt;
&lt;td&gt;~390px&lt;/td&gt;
&lt;td&gt;~170px&lt;/td&gt;
&lt;td&gt;~120px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400×400&lt;/td&gt;
&lt;td&gt;200px&lt;/td&gt;
&lt;td&gt;~195px&lt;/td&gt;
&lt;td&gt;~150px&lt;/td&gt;
&lt;td&gt;~110px&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice how smaller source selections maintain more relative size — this preserves quality when users are already working with smaller images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Output Controls
&lt;/h2&gt;

&lt;p&gt;Beyond &lt;code&gt;quality&lt;/code&gt;, you have fine-grained control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HqCropper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Logarithmic scaling factor&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// JPEG compression (0-1, where 1 is best)&lt;/span&gt;
    &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Output format&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or 'png'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Compression&lt;/strong&gt; is standard JPEG quality (0.0 - 1.0). Combined with &lt;code&gt;quality&lt;/code&gt;, you get precise control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;quality: 1.5&lt;/code&gt; + &lt;code&gt;compression: 0.85&lt;/code&gt; → Balanced (recommended for avatars)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;quality: 2.0&lt;/code&gt; + &lt;code&gt;compression: 0.7&lt;/code&gt; → Smallest files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;quality: 1.01&lt;/code&gt; + &lt;code&gt;compression: 1&lt;/code&gt; + &lt;code&gt;type: 'png'&lt;/code&gt; → Maximum quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Another Image Cropper?
&lt;/h2&gt;

&lt;p&gt;Most existing solutions are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tied to a specific framework (React, Vue, etc.)&lt;/li&gt;
&lt;li&gt;Bloated with dependencies&lt;/li&gt;
&lt;li&gt;Overcomplicated for simple use cases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don't handle the large-to-small image problem well&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works everywhere (vanilla JS, React, Vue, Angular)&lt;/li&gt;
&lt;li&gt;Has zero dependencies&lt;/li&gt;
&lt;li&gt;Focuses on square crops (perfect for avatars)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intelligently handles any source image size&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — pure TypeScript, ~22KB minified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework agnostic&lt;/strong&gt; — works with any stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart scaling&lt;/strong&gt; — logarithmic algorithm for optimal output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag &amp;amp; resize&lt;/strong&gt; — intuitive UI with corner handles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File validation&lt;/strong&gt; — built-in type and size checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — callback-based error reporting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully typed&lt;/strong&gt; — complete TypeScript support&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;hq-cropper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HqCropper&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hq-cropper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HqCropper&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Cropped &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; bytes`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cropper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  React Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HqCropper&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hq-cropper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AvatarUpload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAvatar&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropperRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;HqCropper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;portalSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Avatar"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cropperRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                Upload Avatar
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  All Configuration Options
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HqCropper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Portal (crop area) settings&lt;/span&gt;
        &lt;span class="na"&gt;portalSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;minPortalSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;portalPosition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="c1"&gt;// Output settings&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="c1"&gt;// Validation&lt;/span&gt;
        &lt;span class="na"&gt;maxFileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;allowedTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

        &lt;span class="c1"&gt;// UI labels&lt;/span&gt;
        &lt;span class="na"&gt;applyButtonLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cancelButtonLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cancel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's New in v3.2.0
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bug Fixes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed memory leaks (proper cleanup on modal close)&lt;/li&gt;
&lt;li&gt;Fixed race conditions in canvas operations&lt;/li&gt;
&lt;li&gt;Fixed resize handles in all corners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;New Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;onError&lt;/code&gt; callback for graceful error handling&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxFileSize&lt;/code&gt; and &lt;code&gt;allowedTypes&lt;/code&gt; for file validation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;minPortalSize&lt;/code&gt; to prevent tiny crop areas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM element caching&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requestAnimationFrame&lt;/code&gt; throttling for smooth dragging&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://isalikov.github.io/hq-cropper" rel="noopener noreferrer"&gt;Live Demo &amp;amp; Storybook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/isalikov/hq-cropper" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/hq-cropper" rel="noopener noreferrer"&gt;npm Package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you find this useful, give it a ⭐ on GitHub! Questions? Drop a comment below.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>performance</category>
      <category>showdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
