Іконки виглядають як найпростіша частина дизайн-системи. Поки не відкриваєш проєкт через рік і не знаходиш чотири різні галочки: одну з 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 і кодом. Якщо іконка їх не проходить, вона не їде в репозиторій.
- Один колір, і той -
currentColor. Усередині SVG не має бути жодного хекса. Колір іконка успадковує від тексту поруч, і тоді темна тема, disabled-стан та ховер працюють самі по собі, без окремих варіантів. - Один path. Перед експортом робимо Union або Flatten. Менше нод - менший файл і немає шансу, що половина іконки пофарбується, а половина ні.
- 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), тобто світло-сіра іконка на білому фоні формально не проходить.
Автоматизація: щоб це не розповзлося знову
Ручний експорт працює рівно до моменту, поки дизайнер один. Далі потрібен пайплайн:
- Скрипт тягне іконки з бібліотеки через Figma API (або плагін, якщо не хочеться возитися з токенами).
- SVGO чистить файли, генератор збирає спрайт і файл типів
IconName. - Усе це відкриває PR у репозиторій дизайн-системи - не комітить у main, а саме PR, щоб зміни було видно.
- У 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.