Web razvoj
ReactDizajn sustavTailwind CSSRadix UITypeScriptDizajnerski tokeniMonorepo

Izgradnja React dizajn sustava s dizajnerskim tokenima: Tailwind CSS + Radix UI + TypeScript

AO
Adrijan Omićević
·14 min čitanja

# Što ćete izgraditi#

Izgradit ćete React dizajn sustav s Tailwindom i Radixom koji se može ponovno koristiti kroz više aplikacija, koji podržava teme, pristupačan je po defaultu i verzionira se kao pravi proizvod.

Implementirat ćete:

  • Dizajnerske tokene za boje, razmake i tipografiju kao CSS varijable.
  • Strategiju tematiziranja koja radi kroz React i Next.js aplikacije.
  • Pristupačne primitive uz Radix UI, omotane u opinionated komponente.
  • Pakiranje i distribuciju za ponovno korištenje, plus konvencije za verzioniranje i dokumentaciju.

Ako želite dublje arhitekturne obrasce, kombinirajte ovaj vodič s Arhitektura React komponenti za skalabilan dizajn sustav. Za Tailwind konvencije korištene u nastavku pogledajte Tailwind CSS najbolje prakse. Ako ovo integrirate u produktnu aplikaciju, Početak s Next.js pokriva osnovno postavljanje.

# Preduvjeti#

ZahtjevVerzijaNapomene
Node.js18 ili 20LTS preporučen za monorepo
React18+Radix i moderni alati pretpostavljaju React 18
TypeScript5+Za satisfies, bolju type inference
Tailwind CSS3.4+Tematiziranje temeljeno na varijablama dobro radi
Radix UIlatestKoristite primitive po paketu komponenti
Package managerpnpm preporučenBrži workspaces i stabilniji lockfile

# Preporučeni monorepo raspored#

Dizajn sustav je proizvod. Tretirajte ga tako: odvojeni paketi, strogi API-ji, automatizirana izdanja i predvidljiv build.

Praktičan raspored za više aplikacija:

PutanjaSvrhaObjavljuje se
apps/webNext.js ili React aplikacija koja koristi dizajn sustavNe
apps/adminDruga aplikacija koja koristi isti UI paketNe
packages/tokensIzvor dizajnerskih tokena i generirane CSS varijableDa
packages/uiReact komponente na Radix + TailwindDa
packages/configZajednički alati, Tailwind preset, ESLint pravilaOpcionalno

Zašto odvojiti tokene od UI-ja#

Tokeni su temelj i trebaju biti framework-agnostički. Kasnije ih možete koristiti na webu, u email predlošcima, dokumentaciji ili čak na mobilnim platformama. Držanje tokena u zasebnom paketu smanjuje spregu i čini ih lakšima za audit.

# Korak 1: Definirajte dizajnerske tokene kao CSS varijable#

Token nije “plava 500”. Token je “primary” i može mapirati na različite vrijednosti po temi. Ključno je odvojiti značenje od vrijednosti.

Jednostavan model tokena:

  • Semantički tokeni: --color-bg, --color-fg, --color-primary, --color-danger
  • Tokeni na razini komponente samo kada treba: --button-bg, --card-radius
  • Tokeni skale: koraci razmaka, veličine fonta, radijusi, sjene

Tokeni boja: prvo semantika#

Kreirajte packages/tokens/src/tokens.css:

CSS
:root {
  /* Base */
  --color-bg: 0 0% 100%;
  --color-fg: 222 47% 11%;
 
  /* Brand */
  --color-primary: 222 89% 55%;
  --color-primary-fg: 0 0% 100%;
 
  /* Surfaces */
  --color-muted: 210 40% 96%;
  --color-muted-fg: 215 16% 47%;
 
  /* Feedback */
  --color-danger: 0 84% 60%;
  --color-danger-fg: 0 0% 100%;
 
  /* Borders and focus */
  --color-border: 214 32% 91%;
  --color-ring: 222 89% 55%;
 
  /* Radii */
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
 
  /* Spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
 
  /* Typography */
  --font-sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
  --text-xs: 12px;
  --text-sm: 14px;
  --text-md: 16px;
  --text-lg: 18px;
  --text-xl: 20px;
  --leading-tight: 1.2;
  --leading-normal: 1.5;
}
 
:root[data-theme="dark"] {
  --color-bg: 222 47% 11%;
  --color-fg: 210 40% 98%;
 
  --color-muted: 217 33% 17%;
  --color-muted-fg: 215 20% 65%;
 
  --color-border: 217 33% 17%;
  --color-ring: 222 89% 65%;
}

Ovo koristi HSL komponente kako bi Tailwind mogao čisto primijeniti alpha preko hsl(var(--token) / 0.8).

💡 Savjet: Držite tokene malima i semantičkima. Timovi koji krenu s 200+ tokena boja obično završe s nedosljednim nazivljem i dupliciranom upotrebom. Krenite s 20 do 40 semantičkih tokena i dodajte nove tek kada se pojavi stvarna UI potreba.

Tokeni razmaka: skala je bolja od proizvoljnih brojeva#

Radit ćete brže ako su razmaci predvidljivi. Čest neuspjeh dizajn sustava je “samo stavi padding koji dobro izgleda”.

Koristite skalu koju možete zapamtiti:

  • --space-1 do --space-8
  • izvedite paddings komponenti iz te skale

Tokeni tipografije: kodirajte namjeru, ne samo veličine#

Tokeni poput --text-md su u redu, ali skalabilnija konvencija su tokeni temeljeni na ulozi poput --text-body, --text-caption, --text-heading. Ako vaš proizvod ima samo jedan tipografski stil, krenite s veličinama i proširite kasnije.

# Korak 2: Mapirajte tokene u Tailwind (bez dupliciranja vrijednosti)#

Tailwind treba referencirati tokene. Ne smije ponovno “posjedovati” vrijednosti tokena. To smanjuje divergenciju i čini promjenu teme automatskom.

U packages/config/tailwind-preset.ts:

TypeScript
import type { Config } from "tailwindcss";
 
export const preset = {
  theme: {
    extend: {
      colors: {
        bg: "hsl(var(--color-bg) / <alpha-value>)",
        fg: "hsl(var(--color-fg) / <alpha-value>)",
        primary: "hsl(var(--color-primary) / <alpha-value>)",
        "primary-fg": "hsl(var(--color-primary-fg) / <alpha-value>)",
        muted: "hsl(var(--color-muted) / <alpha-value>)",
        "muted-fg": "hsl(var(--color-muted-fg) / <alpha-value>)",
        border: "hsl(var(--color-border) / <alpha-value>)",
        ring: "hsl(var(--color-ring) / <alpha-value>)",
        danger: "hsl(var(--color-danger) / <alpha-value>)",
        "danger-fg": "hsl(var(--color-danger-fg) / <alpha-value>)",
      },
      borderRadius: {
        sm: "var(--radius-sm)",
        md: "var(--radius-md)",
        lg: "var(--radius-lg)",
      },
      spacing: {
        1: "var(--space-1)",
        2: "var(--space-2)",
        3: "var(--space-3)",
        4: "var(--space-4)",
        6: "var(--space-6)",
        8: "var(--space-8)",
      },
      fontFamily: {
        sans: "var(--font-sans)",
      },
      fontSize: {
        xs: ["var(--text-xs)", { lineHeight: "var(--leading-normal)" }],
        sm: ["var(--text-sm)", { lineHeight: "var(--leading-normal)" }],
        md: ["var(--text-md)", { lineHeight: "var(--leading-normal)" }],
        lg: ["var(--text-lg)", { lineHeight: "var(--leading-normal)" }],
        xl: ["var(--text-xl)", { lineHeight: "var(--leading-tight)" }],
      },
    },
  },
} satisfies Config;

Ovo omogućuje korištenje poput:

  • bg-bg text-fg
  • bg-primary text-primary-fg
  • border-border ring-ring
  • p-4 gap-2 rounded-md

Učinite preset potrošnim u aplikacijama#

U Tailwind configu svake aplikacije uključite preset i skenirajte datoteke UI paketa. U apps/web/tailwind.config.ts:

TypeScript
import type { Config } from "tailwindcss";
import { preset } from "@acme/config/tailwind-preset";
 
export default {
  presets: [preset],
  content: [
    "./src/**/*.{ts,tsx}",
    "../../packages/ui/src/**/*.{ts,tsx}",
  ],
} satisfies Config;

⚠️ Upozorenje: Ako zaboravite uključiti putanje do UI paketa u Tailwind content, production build će tree-shakeati klase vaših komponenti. To je jedan od najčešćih uzroka “radi lokalno, ali ne radi u CI-ju”.

# Korak 3: Dodajte tematiziranje koje radi kroz aplikacije#

Tematiziranje pogonjeno tokenima se svodi na prebacivanje selektora teme i prepuštanje posla CSS varijablama.

Strategija prebacivanja teme#

Koristite data atribut na korijenu dokumenta:

  • Svijetla tema: data-theme="light" ili bez atributa
  • Tamna tema: data-theme="dark"

U Next.js-u ga postavite na razini html elementa kako biste izbjegli “flash”. Točan mehanizam ovisi o vašem stacku, ali dizajn sustav treba zahtijevati samo atribut, ne i specifičnu biblioteku za teme.

Minimalno client-side prebacivanje u React aplikaciji:

TypeScript
export function setTheme(theme: "light" | "dark") {
  document.documentElement.setAttribute("data-theme", theme);
  localStorage.setItem("theme", theme);
}

Zašto je ovo važno za dizajn sustave#

Kad su tokeni varijable, a Tailwind pokazuje na varijable, isti kod komponente renderira:

  • Tamnu temu bez drugog seta klasa
  • Brand temu zamjenom nekolicine vrijednosti tokena
  • Theming po tenant-u u multi-tenant aplikacijama

Tako izbjegavate dupliciranje varijanti komponenti i napuhavanje API-ja.

# Korak 4: Izgradite pristupačne primitive s Radix UI#

Radix UI vam daje pristupačno ponašanje, upravljanje fokusom, interakcije tipkovnicom i ARIA atribute. Vaš posao je dodati stilove, varijante i dosljedne API-je.

Dobro pravilo: koristite Radix za ponašanje i semantiku, Tailwind za izgled, a TypeScript za ograničenja.

Konvencije komponenti koje trebate standardizirati#

Ove konvencije sprječavaju drift API-ja kroz komponente:

KonvencijaPreporučeni standardZašto je važno
StiliziranjeclassName spojen putem utility funkcijeDosljedan “escape hatch”
Varijanteprops variant i sizePredvidljiva upotreba kroz UI
Kompozicijapodrška za asChild kad ima smislaOmogućuje render kao a, button, itd.
Prosljeđivanje ref-aReact.forwardRef za interaktivne elementePotrebno za Radix i forme
Data atributikoristite data-state i data-disabledStiliziranje Radix stanja bez JS-a

Utility: spajanje className#

U packages/ui/src/utils/cn.ts:

TypeScript
export function cn(...classes: Array<string | undefined | false>) {
  return classes.filter(Boolean).join(" ");
}

Držite jednostavno. Ako trebate rješavanje konflikata za Tailwind, uvedite merge utility kasnije, ali izbjegnite kompleksnost rano.

Primjer: Button komponenta kao osnovni “building block”#

Ovo je namjerno kratko i usmjereno na produkciju.

TSX
import * as React from "react";
import { cn } from "../utils/cn";
 
type ButtonVariant = "primary" | "secondary" | "danger";
type ButtonSize = "sm" | "md";
 
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: ButtonVariant;
  size?: ButtonSize;
};
 
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = "primary", size = "md", ...props }, ref) => {
    const base =
      "inline-flex items-center justify-center rounded-md font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none";
 
    const variants: Record<ButtonVariant, string> = {
      primary: "bg-primary text-primary-fg hover:opacity-90",
      secondary: "bg-muted text-fg hover:opacity-90",
      danger: "bg-danger text-danger-fg hover:opacity-90",
    };
 
    const sizes: Record<ButtonSize, string> = {
      sm: "h-9 px-3 text-sm",
      md: "h-10 px-4 text-md",
    };
 
    return (
      <button
        ref={ref}
        className={cn(base, variants[variant], sizes[size], className)}
        {...props}
      />
    );
  }
);
 
Button.displayName = "Button";

Ovo koristi tokene za boje i ring. Kada se tema promijeni, gumb se mijenja bez novog seta klasa.

Primjer: Dialog s Radix primitivama#

Dialog je savršen Radix primjer jer je pristupačnost netrivijalna za ispravno implementirati.

TSX
import * as React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { cn } from "../utils/cn";
 
export function Modal({
  open,
  onOpenChange,
  title,
  children,
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  title: string;
  children: React.ReactNode;
}) {
  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-fg/30" />
        <Dialog.Content
          className={cn(
            "fixed left-1/2 top-1/2 w-[min(92vw,520px)] -translate-x-1/2 -translate-y-1/2",
            "rounded-lg border border-border bg-bg p-4 text-fg shadow"
          )}
        >
          <Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
          <div className="mt-3">{children}</div>
          <Dialog.Close className="mt-4">
            Close
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Vrijednost pristupačnosti ovdje je opipljiva:

  • Escape zatvara dialog.
  • Fokus je “zarobljen”.
  • Pozadina je inertna za screen readere.
  • ARIA uloge i labeli su ispravni.

Ovi detalji su razlog zašto je Radix snažan izbor za temelj dizajn sustava.

ℹ️ Napomena: Radix komponente izlažu stanje preko data atributa poput data-state="open". Koristite to u Tailwind klasama kada želite animacije ili stiliziranje temeljeno na stanju bez dodavanja React statea.

# Korak 5: Token-first obrasci stiliziranja koji se skaliraju#

Kako UI raste, konvencija s najvećim ROI-jem je “token-first utilities”. Umjesto pisanja boja specifičnih za komponentu, slažete bg-bg text-fg border-border i dodajete varijante samo gdje imaju smisla.

Koristite semantičke utility klase u komponentama#

  • Layout: p-4, gap-2, rounded-md
  • Surface: bg-bg, border-border, text-fg
  • Stanja: focus-visible:ring-ring, disabled:opacity-50
  • Feedback: text-danger, bg-danger

Ovo sprječava da hard-coded “brand plava” procuri u komponente. Također omogućuje re-branding bez refaktora.

Kada dodati tokene na razini komponente#

Dodajte tokene na razini komponente samo ako:

  • Komponenta ima jedinstven zahtjev stiliziranja koji se ne dijeli drugdje.
  • Više proizvoda treba različite stilove komponenti uz zadržavanje iste semantičke teme.

Primjer: ako vaš Card treba posebnu sjenu po proizvodu, uvedite --card-shadow umjesto da ugradite Tailwind shadow klasu u komponentu.

# Korak 6: Zapakirajte dizajn sustav za ponovno korištenje kroz aplikacije#

Razlika između foldera s komponentama i dizajn sustava je distribucija i disciplina. Pakiranje vas prisiljava da definirate javni API.

Što izvoziti#

Izložite čistu površinu API-ja:

PaketIzvozPrimjer
@acme/tokensCSS varijable i dokumentaciju tokenadist/tokens.css
@acme/uiKomponente i tipoveButton, Modal, Input
@acme/configTailwind presetpreset

Strategija builda#

Držite build dosadnim:

  • @acme/tokens objavljuje CSS.
  • @acme/ui kompajlira TypeScript i isporučuje CSS kroz Tailwind upotrebu u aplikacijama potrošačima.

Ako želite da UI paket isporuči prebuilt CSS, napravite to tek kada imate jasnu potrebu integracije. Isporuka CSS-a može pomoći potrošačima koji ne koriste Tailwind, ali dodaje kompleksnost oko bundlanja i dedupinga.

Osigurajte da potrošači učitaju tokene#

Dodajte jedan import u globalni CSS aplikacije, npr. u Next.js app/globals.css:

CSS
@import "@acme/tokens/tokens.css";
 
@tailwind base;
@tailwind components;
@tailwind utilities;
 
body {
  background: hsl(var(--color-bg));
  color: hsl(var(--color-fg));
}

# Korak 7: Konvencije verzioniranja koje sprječavaju kaos pri nadogradnjama#

Ako više aplikacija ovisi o vašem UI-ju, verzioniranje postaje operativno pitanje. Semantičko verzioniranje je nužno, ali nije dovoljno. Trebate i pravila što se smatra breaking promjenom.

Preporučena pravila semantičkog verzioniranja#

PromjenaBump verzijePrimjer
Breaking promjena API-jaMajorPreimenovanje propa, promjena default ponašanja
Unatrag kompatibilna značajkaMinorNova komponenta ili ne-breaking varijanta
Samo bugfixPatchIspravak focus ringa, bug u klasama, bug u tipovima

Praktične breaking promjene koje mnogi timovi promaše:

  • Promjena default vrijednosti variant ili size.
  • Promjena značenja tokena čak i ako naziv ostane isti.
  • Promjena DOM strukture na koju se testovi oslanjaju.

Automatizirajte s Changesets#

Changesets je dobar default za monorepo jer forsira human-readable release noteove i veže ih uz PR-ove.

Tipičan workflow:

  1. 1
    Dodajte changeset datoteku po PR-u koji utječe na objavljene pakete.
  2. 2
    Merge u main.
  3. 3
    CI objavljuje pakete i generira changelog.

Vaš minimalni standard za svako izdanje:

  • Sažetak
  • Migracijske napomene za breaking promjene
  • Zahvaćeni paketi

🎯 Ključna poruka: Najbrži način da izgubite povjerenje u dizajn sustav je isporučivanje “tihih” breaking promjena. Strogo verzioniranje i changelogovi nisu birokracija, nego pouzdanost proizvoda.

# Korak 8: Dokumentacija koju developeri stvarno koriste#

Dokumentacija treba odgovoriti na “kako ovo sigurno koristiti u produkciji” u manje od dvije minute po komponenti.

Minimalna dokumentacija po komponenti#

SekcijaŠto uključitiPrimjer
SvrhaKada koristiti, kada neSmjernice Button vs LinkButton
APIProps, defaulti, varijantevariant, size, disabled ponašanje
PristupačnostNapomene za tipkovnicu i screen readereRedoslijed fokusa, labeli, očekivanja aria-*
PrimjeriCopy-paste isječciPrimary, secondary, destructive upotreba
TokeniO kojim tokenima ovisi--color-primary, --color-ring

Praktičan format dokumentacije#

Držite dokumentaciju u repozitoriju blizu izvora. Čest obrazac:

  • packages/ui/src/button/button.mdx
  • packages/ui/src/modal/modal.mdx

Čak i ako kasnije prijeđete na punu docs stranicu, početak blizu koda održava dokumentaciju ažurnom jer se mijenja u istom PR-u.

Uključite primjere korištenja vezane uz Next.js#

Budući da mnogi timovi rade s Next.js, uključite barem jedan primjer po komponenti koji radi s Next.js App Router obrascima. Ako trebate osnovnu strukturu projekta, referencirajte Početak s Next.js.

# Korak 9: Produkcijske provjere: pristupačnost, dosljednost i usvajanje#

Dizajn sustav uspijeva kada je usvajanje jednostavno, a regresije rijetke.

Provjere pristupačnosti koje možete standardizirati#

  • Navigacija samo tipkovnicom za sve interaktivne komponente.
  • Vidljivo fokus stanje na svim fokusabilnim elementima.
  • Focus trapping i escape handling za modal i popover.
  • Kontrole forme moraju imati labele, opise i obrasce za error poruke.

Praktičan KPI za pratiti: smanjiti dizajnom povezane bugove. Mnogi timovi navode da standardizacija na pristupačnu biblioteku komponenti značajno smanjuje UI regresije jer se ponašanje više ne re-implementira po featureu.

Provjere dosljednosti#

  • U zajedničkim komponentama koristite tokene isključivo za boje i razmake.
  • Nema hard-coded hex boja u @acme/ui.
  • Varijante i veličine usklađene kroz komponente.
  • Put depreciranja za stare komponente, ne nagla uklanjanja.

Za granice komponenti i pravila ponovne upotrebe kroz timove, uskladite se s obrascima iz Arhitektura React komponenti za skalabilan dizajn sustav.

Pravila održivosti Tailwinda#

Ako Tailwind stringovi utility klasa postanu teško čitljivi:

  • Izvucite ih u male konstante unutar komponente.
  • Izbjegavajte “mega-stringove” klasa koji miješaju layout, stanje i temu u jednom redu.
  • Držite varijante eksplicitnima.

Za kompletan checklist, pogledajte Tailwind CSS najbolje prakse.

# Ključne poruke#

  • Definirajte dizajnerske tokene kao semantičke CSS varijable i neka ih Tailwind referencira kako bi tematiziranje postalo zamjena tokena, a ne prepisivanje komponenti.
  • Koristite Radix UI za pristupačno ponašanje i omotajte primitive u opinionated React komponente s dosljednim obrascima variant, size i className.
  • Zapakirajte tokene, Tailwind preset i UI komponente odvojeno kako bi se više aplikacija moglo nadograđivati neovisno i sigurno.
  • Provodite semantičko verzioniranje uz automatizirane changelogove kako biste spriječili tihe breaking promjene u aplikacijama potrošačima.
  • Dokumentirajte svaku komponentu sa svrhom, API-jem, napomenama o pristupačnosti, ovisnostima o tokenima i copy-paste primjerima kako biste poboljšali usvajanje.

# Zaključak#

React dizajn sustav s Tailwindom i Radixom postaje održiv kada su tokeni jedini izvor istine, Radix pruža pristupačne primitive, a vaš UI paket isporučuje dosljedan API uz disciplinirano verzioniranje.

Ako želite da vam Samioda pomogne izgraditi ili modernizirati produkcijski dizajn sustav, uključujući arhitekturu tokena, komponente temeljene na Radixu i automatizirana izdanja kroz više aplikacija, javite se putem naše web stranice i definirat ćemo praktičan plan uvođenja.

FAQ

Share
A
Adrijan OmićevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.