Design System w Next.js od zera: Tailwind, CVA i Radix UI
Poznaj nowoczesną architekturę komponentów. Zobacz, jak połączyć Tailwind, Radix UI i CVA, aby stworzyć skalowalny system projektowy od zera.
Maciej Sala
Founder StriveLab
4 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 17 lipca 2026)
Po co budować własny design system w Next.js?
Zyskujesz na tym potrójnie: spójny wygląd, szybsze pisanie i mniej dziwnych bugów CSS, które biorą się z ręcznego powielania klas. Zestaw Tailwind + CVA + Radix UI sprawdza się tu od lat — łączy utility-first CSS z typowanymi wariantami i mechanizmami dostępności dla interaktywnych komponentów.
Stack design systemu: Tailwind, CVA i Radix UI
Tailwind CSS — utility classes do stylowania
— typowane warianty komponentów
Radix UI — (bez stylów) komponenty z wbudowaną dostępnością (ARIA)
Radix nadal udostępnia osobne pakiety @radix-ui/react-*, ale pakiet radix-ui
upraszcza zależności i pozwala importować prymitywy z jednego miejsca.
Powyższe polecenie zakłada, że Tailwind CSS jest już skonfigurowany w projekcie
Next.js.
Helper cn() jako fundament design systemu
Funkcja cn() łączy clsx (warunkowe klasy) z tailwind-merge (rozwiązywanie konfliktów):
Code
// lib/utils.tsimport { type ClassValue, clsx } from 'clsx'import { twMerge } from 'tailwind-merge'export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs))}
Dlaczego to ważne:
Code
// Bez tailwind-merge — konflikty klas<div className="bg-red-500 bg-blue-500" /> // Która wygra? Nieprzewidywalne.// Z cn() — ostatnia klasa wygrywa<div className={cn('bg-red-500', 'bg-blue-500')} /> // bg-blue-500
Komponent Button z CVA i wariantami
CVA pozwala definiować warianty komponentu jako obiekt TypeScript — z pełnym autocompletem i type safety:
Radix UI to zestaw headless komponentów, które dostarczają zachowanie i mechanizmy dostępności (zarządzanie focusem, ARIA, klawiatura), ale nie narzucają przy tym stylów. Sami dodajemy wygląd przez Tailwind, a przy użyciu prymitywów rozwiązujemy trudną część techniczną, nadając także poprawne etykiety, zrozumiałą treść i kontrast.
Radix automatycznie obsługuje zamykanie klawiszem Escape, blokadę focusu wewnątrz modala i jego przywracanie po zamknięciu. Dialog.Title oraz Dialog.Description dostarczają powiązania potrzebne czytnikom ekranu. Nie pomijaj tytułu; jeśli świadomie rezygnujesz z opisu, zgodnie z dokumentacją ustaw na Dialog.Content wartość aria-describedby={undefined}.
Teraz zamiast text-blue-600 używasz text-brand-600, a tokeny są dostępne także jako zwykłe zmienne CSS, np. var(--color-brand-600). W projekcie z Tailwind CSS 3 analogiczny motyw definiuje się w tailwind.config, ale nie jest to już główna ścieżka konfiguracji Tailwinda 4. Jeśli wartości mają zmieniać się między jasnym i ciemnym motywem, trzymaj właściwe kolory w zwykłych zmiennych CSS, a następnie mapuj je na nazwy @theme inline.
// components/ui/index.tsexport { Button, buttonVariants } from './button'export { Input } from './input'export { Badge } from './badge'export { Card } from './card'export { Modal } from './modal'
Plik zbiorczy jest wygodny, ale też nie jest obowiązkowy. W kodzie trafiającym do Client Components bezpośrednie importy, np. @/components/ui/button, ułatwiają kontrolę granic klient–serwer i analizę paczki. Niezależnie od stylu importu eksportuj tylko elementy należące do publicznego API design systemu.
Dokumentacja i testy komponentów
Sam katalog components/ui nie czyni jeszcze systemu projektowego dojrzałym. Każdy komponent powinien posiadać udokumentowane warianty, stany (takie jak hover, focus-visible, disabled, błędy czy stany ładowania) oraz przykłady poprawnego użycia. Możesz utrzymywać tę dokumentację w Storybooku lub w wydzielonej sekcji aplikacji i kluczowe jest, aby zespół miał jedno, centralne miejsce prezentujące publiczne API wszystkich komponentów.
Testuj przede wszystkim własne kontrakty, przykładowo, czy Button przekazuje atrybuty i ref, czy błąd Input jest powiązany przez aria-describedby, czy modal ma tytuł i można go obsłużyć klawiaturą. Do testów zachowania wystarczą Testing Library i runner używany w projekcie, a automatyczny audyt axe warto uzupełnić ręcznym testem klawiaturą. Testy regresji wizualnej pomagają wykryć niezamierzone zmiany tokenów i wariantów, których zwykły test DOM nie zauważy.
Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.
Wariant Radix w shadcn/ui korzysta z bardzo podobnego stosu: Tailwind, CVA i prymitywy Radix. shadcn/ui obsługuje jednak także Base UI, więc nie każdy nowy projekt używa już Radix. Różnica pozostaje w modelu dystrybucji: CLI dodaje kod komponentów do Twojego projektu, a nie dostarcza zamkniętej biblioteki. Budowanie od zera daje większą kontrolę nad API, ale start od shadcn/ui i późniejsza modyfikacja komponentów również jest sensowną ścieżką.
Czy mogę użyć CVA bez Radix?
Tak. CVA to narzędzie wyłącznie do zarządzania wariantami komponentów i działa z dowolnym elementem — przyciskiem, badge'em, kartą. Radix dokłada warstwę dostępności i zachowania (focus, ARIA, klawiatura), ale nie jest wymagany. Proste, statyczne komponenty bez interakcji nie potrzebują Radix; sięgasz po niego dopiero przy modalach, dropdownach czy tabach, gdzie dostępność jest trudna do zrobienia ręcznie.
Czy design system spowalnia bundle?
Może, zwłaszcza gdy wiele interaktywnych prymitywów Radix trafia do Client Components. Tailwind generuje CSS podczas builda, ale CVA, tailwind-merge i kod interakcji Radix mogą znaleźć się w paczce klienta. Importuj tylko używane elementy, utrzymuj granice use client możliwie nisko i mierz wynik analizatorem paczki zamiast zakładać, że narzut zawsze będzie pomijalny.
Po co helper cn() zamiast zwykłego łączenia klas?
Bo samo sklejanie stringów klas Tailwinda prowadzi do konfliktów. Gdy w jednym miejscu masz bg-red-500, a w drugim nadpisujesz bg-blue-500, bez tailwind-merge wynik jest nieprzewidywalny — zależy od kolejności w CSS, nie w kodzie. cn() łączy clsx (warunkowe klasy) z tailwind-merge, dzięki czemu ostatnia klasa danej właściwości zawsze wygrywa, tak jak intuicyjnie tego oczekujesz.
Od ilu komponentów zacząć budowę design systemu?
Od minimalnego zestawu, który faktycznie wykorzystasz — zwykle Button, Input, Card i Badge. To wystarcza, żeby ujednolicić większość interfejsu, a jednocześnie nie wpadasz w pułapkę budowania komponentów „na zapas", których nikt nie użyje. Design system rozbudowuj reaktywnie: gdy trzeci raz piszesz ten sam wzorzec ręcznie, to znak, że czas zrobić z niego komponent.
O autorze
Maciej Sala
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.
WordPress z Elementorem albo Divi to najszybszy sposób, żeby postawić stronę, ale też najszybszy, żeby ją spowolnić, akurat kiedy zacznie zarabiać. Wizualny edytor, który był początkowo wygodny, z czasem będzie ciągnął w dół Core Web Vitals , a z nimi pozycje w Google i konwersję. Ten artykuł pokazuje, jak zdiagnozować problem, wybrać między Astro a Next.js i przeprowadzić migrację bez utraty wypracowanego z trudem SEO.
Maciej Sala
Founder StriveLab
Mamy tu typową stronę usługową zbudowaną w Next.js, wyposażoną w Google Analytics, Google Tag Manager , prosty formularz kontaktowy, galerię zdjęć i animacje. Na początku testy PageSpeed Insights pokazały wyniki: 67/100 na urządzeniach mobilnych i 89/100 na komputerach stacjonarnych.
Maciej Sala
Founder StriveLab
W tym artykule pokazuję siedem wzorców, które sam stosuję produkcyjnie — w StriveLab https://strivelab.pl/tworzenie-stron-internetowych/react/ , Army Builder https://army-builder.com , i u klientów. Każdy z nich rozwiązuje konkretny problem, który w 2026 roku masz rozwiązywać lepiej niż pięć lat temu, bo TypeScript i React dostały nowe narzędzia.