У нашому дописі про структурування frontend-частини у Laravel Inertia React застосунках ми розповідали, чому shadcn/ui є нашим стандартом для швидкого старту. Це дозволяє миттєво перейти від ідеї до релізу, а з часом ми переносимо компоненти до власної папки common, адаптуючи їх під потреби проєкту.
Цей підхід довго нас виручав. Проте веб-технології поступово розв'язують проблеми, які раніше змушували нас використовувати бібліотеки на кшталт shadcn та Radix. Нативні dialogs, popovers, anchor positioning та кастомізовані selects вже з'являються у браузерах. Багато з цих функцій дублюють можливості примітивів Radix, що лежать у нашій директорії shadcn.
Це спонукає нас переглянути підхід до розробки frontend у найближчому майбутньому. Ми не плануємо миттєво відмовлятися від shadcn, але ставимо фундаментальне питання: «Чи можемо ми створювати власні базові компоненти на основі нативних HTML/CSS замість сторонніх бібліотек?». Йдеться про компоненти, що працюють незалежно від фреймворку, потребують менше JavaScript та мають вбудовану доступність (accessibility) на рівні браузера. Чи можемо ми покращити якість коду та DX (developer experience) без компромісів?
У цій статті ми розберемо, що змінилося, які можливості відкрилися та куди ми рухаємося далі.
З чого все починалося: shadcn та Radix як фундамент
Останні кілька років кожен новий проєкт Spatie стартував однаково: ми розгортали застосунок, встановлювали shadcn/ui та отримували готовий набір компонентів: діалоги, випадаючі списки, тултіпи. Усі вони побудовані на Radix та мають гарну доступність «з коробки».
Як ми вже згадували, API, які пропонують shadcn та Radix, іноді занадто низькорівневі, тому ми створюємо над ними власні абстракції. Наприклад, складний Select із купою підкомпонентів (SelectTrigger, SelectValue тощо) ми перетворюємо на лаконічний компонент з одним options prop. Це зручно, але фактично ми підтримуємо «обгортку над обгорткою» того, що браузер мав би обробляти нативно.
Справжня ціна такого підходу проявляється з часом. Radix використовує JavaScript для утримання фокуса, навігації клавіатурою, блокування прокрутки та позиціонування. Окремо це не проблема, але сумарно це значний обсяг коду для дублювання поведінки, яку HTML вже вміє виконувати самостійно. Ба більше, Radix більше не оновлюється, тому старі помилки доводиться виправляти дедалі складнішими методами.
Сьогодення: браузери наздоганяють
Елемент dialog: модальні вікна без зайвих зусиль
Раніше створення модального вікна вимагало складної логіки: портали для рендерингу поза DOM-деревом, focus trap для клавіатури, блокування body та створення overlay. Саме тому ми обирали Radix.
Нативний елемент dialog, що став стандартом ще у 2022 році, робить усе це автоматично. Метод showModal() виносить вікно на верхній шар (top layer), робить іншу частину сторінки інертною, утримує фокус та закриває вікно клавішею Escape. Псевдоелемент ::backdrop дозволяє легко стилізувати фон. Це повний функціонал бібліотеки модальних вікон, вбудований у браузер із нульовим використанням JavaScript.
Ми не стверджуємо, що нативний dialog зараз покриває всі складні випадки, як-от анімовані переходи чи вкладені вікна. Проте для стандартних форм та підтверджень dialog підходить ідеально. Це дозволяє нам реалізовувати компоненти з мінімальним обсягом коду.
Popover API: нативні випадаючі меню та підказки
Popover API став загальнодоступним на початку 2025 року. Хоча підтримка старих браузерів залишається питанням, це суттєво змінює підхід до розробки інтерфейсів. API дозволяє створювати меню та тултіпи декларативно.
Достатньо додати атрибут popovertarget до кнопки та popover до блоку з контентом:
<button popovertarget="actions">Дії</button>
<div id="actions" popover>
<!-- контент -->
</div>
Браузер сам дбає про z-index, закриття вікна при кліку зовні (light dismiss) та підтримку клавіші Escape.
Зараз існують три режими: popover="auto" (автоматичне закриття інших вікон), popover="manual" (для сповіщень) та новий popover="hint" (для тултіпів). Останній вирішує давню проблему нашарування елементів, яка часто виникала у великих проєктах.
Invoker Commands: взаємодія без event listeners
Invoker Commands API додає атрибути commandfor та command, що дозволяють кнопкам керувати іншими елементами без JavaScript:
<button commandfor="confirm" command="show-modal">Видалити</button>
<dialog id="confirm">
<p>Ви впевнені?</p>
<button commandfor="confirm" command="close">Скасувати</button>
</dialog>
Хоча поки що функція доступна переважно у Chrome та Edge, вектор розвитку очевидний: браузери роблять логіку «натиснув кнопку — щось відкрилося» вбудованою частиною HTML. Саме цю роботу зараз виконують React та Radix.
Майбутнє: чого ще чекати
Interest Invokers: інтерфейс за наведенням без JavaScript
Показ інформації при наведенні курсору (hover) — поширений патерн. Сьогодні це вимагає обробників mouseenter/mouseleave та таймерів для уникнення мерехтіння. У нашому коді це зазвичай роблять компоненти Radix/shadcn.
Новий Interest Invoker API вводить атрибут interestfor. У поєднанні з popover="hint" це робить тултіпи повністю декларативними. Браузер сам керує затримками та підтримує довге натискання на тачскрінах. Бібліотеки, які ми використовуємо сьогодні для тултіпів, незабаром можуть стати непотрібними.
CSS Anchor Positioning: прощавай, Floating UI
Будь-який тултіп чи випадаючий список має бути прив'язаний до тригера. Логіка позиціонування, врахування країв екрана та скролу — це головна причина існування таких бібліотек, як Floating UI. Ми використовуємо її через Radix, бо писати таку математику з нуля занадто складно.
Проте Anchor Positioning змінює правила гри:
.trigger {
anchor-name: --menu-trigger;
}
.menu {
position: absolute;
position-anchor: --menu-trigger;
top: anchor(bottom);
left: anchor(left);
position-try-fallbacks: flip-block, flip-inline;
}
Властивість position-try-fallbacks автоматично знаходить найкраще місце для елемента, якщо він не вміщується. Жодного JavaScript для вимірювання координат більше не потрібно.
Кастомізований select: кінець складних компонентів
Нативний select завжди був «болем» для розробників через неможливість нормальної стилізації. Chrome 135 представив appearance: base-select, що дозволяє повністю змінювати вигляд нативного списку, зберігаючи його доступність та логіку роботи з клавіатурою. Це вбивця кастомних випадаючих списків на JavaScript.
Що це означає для розробки застосунків
Ми не збираємося видаляти shadcn зі старих проєктів вже завтра. Деяким API ще бракує широкої підтримки браузерів, а готові бібліотеки залишаються найшвидшим способом отримати якісний інтерфейс.
Однак зараз чудовий час переглянути вміст папки common. Замість того, щоб обгортати примітиви Radix, ми можемо створювати власні компоненти: Dialog на основі нативного елемента, Popover з відповідними атрибутами, Select з використанням base-select. Такі компоненти не будуть жорстко прив'язані до React — вони базуватимуться на HTML та CSS, а фреймворк стане лише тонким шаром логіки. Коли браузер сам керує фокусом та доступністю, нам не потрібно це тестувати — воно просто працює.
Шлях попереду
Специфікації HTML та CSS зараз розвиваються швидше, ніж будь-коли. Те, що раніше вимагало об'ємних JavaScript-бібліотек, стає частиною веб-платформи. У Spatie ми вже досліджуємо, як може виглядати набір базових компонентів, що максимально використовують можливості сучасних браузерів.
Shadcn, Radix та Base-UI стали для нас надійним фундаментом, коли можливостей платформи не вистачало. Тепер платформа наздогнала їх. Час рухатися назустріч змінам.