Іконки виглядають як найпростіша частина дизайн-системи. Поки не відкриваєш проєкт через рік і не знаходиш чотири різні галочки: одну з Material, одну намальовану руками під конкретний екран, одну з Figma Community і ще одну, яку хтось вставив картинкою через <img>.

У коді ситуація не краща. Десь інлайновий SVG прямо в JSX, десь іконочний шрифт, який лишився з 2019-го, десь React-компонент на сорок рядків із захардкодженим fill="#111827", який не вміє змінювати колір у темній темі.

Це відбувається тому, що іконки ламаються тихо. Кнопку хтось рев'юїть, у неї є пропси і сторібук. Іконку просто перетягують у макет. Ніхто не помічає проблеми, поки набір не розповзеться настільки, що дизайнер уже не знає, яку галочку брати.

Нижче - те, як зібрати іконки в систему: від сітки у Figma до компонента Icon у коді.

Крок 1. Сітка і оптичний розмір

Базовий розмір іконки - 24×24. Це найпоширеніший розмір в інтерфейсах, і від нього легко рахувати решту. Усередині цих 24 пікселів іконка не займає весь квадрат: лишаємо 2 пікселі відступу з кожного боку, тобто реальна зона малювання - 20×20.

Далі - keyline shapes. Це чотири базові форми, у які має вписуватися будь-яка іконка, щоб набір виглядав однаково вагомим:

  • квадрат 18×18 - для прямокутних форм (документ, картка)
  • коло діаметром 20 - для круглих (аватар, годинник)
  • вертикаль 20×16 - для високих форм
  • горизонталь 16×20 - для широких

Без цього коло завжди виглядає меншим за квадрат того ж розміру. Це оптика, не математика.

Головна помилка - думати, що менші розміри це просто зменшений 24-піксельний оригінал. Іконка на 16 пікселів, отримана масштабуванням, стає мутною: обведення 1.5px перетворюється на 1px, деталі злипаються. Якщо вам реально потрібен розмір 16 - малюйте окрему сітку з товщиною обведення 1.5 і меншою кількістю деталей. Зазвичай вистачає двох наборів: 24 і 16.

Крок 2. Три правила, без яких іконка не потрапляє в систему

Ці правила - контракт між Figma і кодом. Якщо іконка їх не проходить, вона не їде в репозиторій.

  1. Один колір, і той - currentColor. Усередині SVG не має бути жодного хекса. Колір іконка успадковує від тексту поруч, і тоді темна тема, disabled-стан та ховер працюють самі по собі, без окремих варіантів.
  2. Один path. Перед експортом робимо Union або Flatten. Менше нод - менший файл і немає шансу, що половина іконки пофарбується, а половина ні.
  3. viewBox 0 0 24 24, без width і height у файлі. Розмір задає компонент, а не SVG. Інакше ви не зможете відрендерити ту саму іконку на 20 пікселів.

Окремо про обведення. Якщо система лінійна, у вас є вибір: лишити stroke і зафіксувати розмір, або зробити Outline stroke і працювати із заливкою. Другий варіант нудніший для дизайнера, але значно надійніший для коду - залита фігура коректно фарбується через fill: currentColor і не тоншає при масштабуванні.

І назви. Називайте іконку за формою, а не за дією: trash, а не delete. Бо той самий кошик через півроку використають для «очистити фільтри», і назва почне брехати. Дію задає місце використання, а не сама іконка.

Крок 3. Експорт з Figma без болю

Кожна іконка - окремий компонент 24×24 у файлі бібліотеки, з назвою у kebab-case: arrow-right, chevron-down, trash. Структура сторінки - плоский список, без вкладених груп: вкладеність все одно розсиплеться при експорті.

У налаштуваннях експорту вимкніть «Include id attribute» - Figma інакше насипле у файл айдішники шарів, які нікому не потрібні. Далі проганяємо все через SVGO:

npx svgo -f ./icons-raw -o ./icons --config svgo.config.js

У конфізі вимикаємо removeViewBox (без нього іконка перестане масштабуватися), вмикаємо removeDimensions і додаємо заміну всіх кольорів на currentColor. Типова іконка з Figma важить кілька кілобайтів, після чистки лишаються сотні байтів, і це без втрати якості - просто зникає сміття, яке генерує редактор.

Крок 4. Три способи доставити іконки в код

Тут зазвичай і починаються суперечки з розробниками. Насправді варіантів усього три, і вибір залежить від кількості іконок.

SVGR: кожна іконка стає React-компонентом. Найзручніший варіант для розробника - автокомпліт, типізація, tree-shaking. Мінус один, але серйозний: якщо імпортувати іконки через барел-файл (import { ArrowRight } from './icons'), у бандл може поїхати весь набір. Імпортуйте пофайлово.

SVG-спрайт: один файл, усередині <symbol> з айді. Іконка вставляється через <use href="/icons.svg#trash"/>. Спрайт кешується браузером окремо від JS, HTML лишається крихітним, а дублікати однієї іконки нічого не коштують. Це найкращий варіант, коли іконок багато або коли назва іконки приходить з даних.

Іконочний шрифт. У 2026-му - ні. Проблеми з доступністю (скрінрідер читає гліф як випадковий символ), спалах порожнечі поки шрифт вантажиться, неможливість зробити двоколірну іконку.

Практичне правило: до сорока статичних іконок - SVGR, більше ста або динамічні за іменем - спрайт. Посередині працює будь-що, тому беріть те, з чим команді зручніше.

Крок 5. API компонента Icon

Незалежно від способу доставки, у коді має бути один компонент із передбачуваними пропсами:

<Icon name="trash" size={20} />
<Icon name="check" label="Оплачено" />

Три пропси покривають 95% випадків. name - типізований union усіх наявних іконок, щоб опечатка падала ще в редакторі. size - не довільне число, а перелік з 16, 20 і 24: вільний розмір миттєво повертає вас до мутних іконок. label - текстовий опис, і саме він вмикає логіку доступності.

Кольору серед пропсів немає навмисне. Колір іконка бере від currentColor батьківського елемента, тож фарбуємо контейнер, а не іконку.

Доступність: коли іконка це контент, а коли просто прикраса

Іконка поруч із текстом (кнопка «Видалити» з кошиком) - декоративна. Вона дублює те, що вже написано, і скрінрідер має її ігнорувати: aria-hidden="true" плюс focusable="false" - друге лишається в шаблоні як страховка, щоб SVG випадково не потрапив у таб-послідовність.

Іконка без тексту (кнопка тільки з кошиком) несе сенс. Але підпис вішається на кнопку, а не на <svg>: <button aria-label="Видалити">. Це та помилка, яку я найчастіше бачу в аудитах - aria-label приліплений до самої іконки, де його половина скрінрідерів просто не прочитає.

Два числа, які варто запам'ятати. Мінімальна зона натискання - 24×24 CSS-пікселі (WCAG 2.2, Target Size, рівень AA), тому іконка 16px усередині кнопки все одно потребує падінгів. І контраст значущої графіки до фону - мінімум 3:1 (WCAG 1.4.11), тобто світло-сіра іконка на білому фоні формально не проходить.

Автоматизація: щоб це не розповзлося знову

Ручний експорт працює рівно до моменту, поки дизайнер один. Далі потрібен пайплайн:

  1. Скрипт тягне іконки з бібліотеки через Figma API (або плагін, якщо не хочеться возитися з токенами).
  2. SVGO чистить файли, генератор збирає спрайт і файл типів IconName.
  3. Усе це відкриває PR у репозиторій дизайн-системи - не комітить у main, а саме PR, щоб зміни було видно.
  4. У CI - лінт іконок: скрипт падає, якщо у файлі знайшовся хекс-колір, немає viewBox або розмір не 24.

Останній пункт важливіший, ніж здається. Правила, які не перевіряються автоматично, перестають виконуватися приблизно через місяць. Правило, яке роняє білд, живе роками.

І ще одне: публікуйте іконки окремим npm-пакетом від решти компонентів. Іконки оновлюються значно частіше, і ніхто не хоче піднімати мажорну версію всієї дизайн-системи через додану стрілочку.

Чекліст

  • Сітка 24×24 із зоною малювання 20×20 і keyline shapes
  • Окремий набір для 16px, а не масштабований 24px
  • Усередині SVG немає хексів, тільки currentColor
  • Один path, viewBox на місці, width і height прибрані
  • Назви за формою, у kebab-case, плоским списком
  • Один компонент Icon з пропсами name, size, label
  • Декоративні іконки сховані від скрінрідера, зона натискання від 24px
  • Лінт іконок у CI і окремий пакет для релізів

Система іконок - це приблизно два дні роботи на старті і кілька годин на пайплайн. Взамін ви перестаєте кожні пів року проводити ревізію набору і пояснювати, чому в застосунку три різні хрестики.

Я розбираю подібні речі на живих проєктах на YouTube-каналі UX Hero.