Web razvoj
Next.jsDizajn sustavStorybookTailwind CSSRadix UIPristupačnostMonorepoTestiranje

Izrada dizajn sustava u Next.js s Radix UI, Tailwindom i Storybookom: vodič od početka do kraja za 2026.

AO
Adrijan Omićević
·13 min čitanja

# Što ćete izgraditi i zašto je to važno#

Dizajn sustav u Next.js proizvodu nije Figma biblioteka. To je verzionirani UI paket s tokenima, komponentama, zadanim postavkama pristupačnosti, dokumentacijom i testovima koji nameću konzistentnost.

Ovaj vodič pokazuje pristup od početka do kraja za izgradnju stacka Next.js design system Storybook Tailwind Radix koji se skalira: tokeni i theming, komponente bazirane na Radixu, Storybook dokumentacija, integracija u product codebase i governance kroz lintanje i vizualnu regresiju.

Ako želite dublju pozadinu o tokenima i arhitekturi, prvo pročitajte:

# Pregled arhitekture: Monorepo, paketi i tok izdanja#

Najbrži način da dizajn sustav ostane upotrebljiv jest da ga je lako konzumirati, a teško pogrešno koristiti. Monorepo s workspace paketima obično postiže taj balans.

Preporučena struktura projekta#

PutanjaSvrhaNapomene
apps/webNext.js aplikacija proizvodaKoristi samo @acme/ui komponente
packages/uiKomponente dizajn sustavaRadix primitive, Tailwind stiliziranje, exportani API
packages/tokensTokeni i Tailwind presetCentralni source of truth za boje, razmake, radijuse
packages/eslint-configPravila konzistentnostiOgraniči raw HTML, nametni granice importa
packages/storybookStorybook hostMože biti unutar packages/ui ili kao zasebna app

Ova struktura podržava:

  • Neovisno verzioniranje @acme/ui
  • Dijeljene tokene između aplikacija
  • Centralizirana lint pravila
  • Storybook kao jedinstenu dokumentacijsku stranicu

Odabiri alata koji dobro rade u 2026.#

PodručjePreporukaZašto
WorkspacespnpmBrze instalacije, stroga izolacija dependencija
Build sustavTurborepoCache i paralelni buildovi za CI
VersioningChangesetsPredvidljiva izdanja i generiranje changeloga
Vizualna regresijaChromatic ili Playwright screenshotoviHvata UI drift prije deploya
A11y automatizacijaStorybook a11y + testing-libraryRano pronalazi česte WCAG probleme

🎯 Ključna poruka: Dizajn sustav tretirajte kao proizvod: verzionirajte ga, testirajte, dokumentirajte i provodite kroz CI.

# Korak 1: Design tokeni kao izvor istine#

Tokeni moraju biti upotrebljivi i iz Tailwind utilityja i iz stilova komponenti. Praktičan pristup je definirati tokene u CSS varijablama, a zatim mapirati Tailwind na te varijable.

Strategija tokena: CSS varijable plus Tailwind mapiranje#

Koristite semantičke tokene poput --color-bg, a ne sirove vrijednosti poput --blue-500. Semantički tokeni podržavaju theming i smanjuju refaktore.

Primjer token datoteke u packages/tokens/src/styles/tokens.css:

CSS
:root {
  --color-bg: 255 255 255;
  --color-fg: 17 24 39;
  --color-primary: 59 130 246;
 
  --radius-sm: 6px;
  --radius-md: 10px;
 
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
}
 
[data-theme="dark"] {
  --color-bg: 17 24 39;
  --color-fg: 243 244 246;
  --color-primary: 96 165 250;
}

Koristite RGB triplete kako bi Tailwind mogao primijeniti opacity putem rgb(var(--token) / alpha).

Tailwind preset u tokens paketu#

Kreirajte packages/tokens/src/tailwind-preset.ts:

TypeScript
import type { Config } from "tailwindcss";
 
export const preset: Config = {
  theme: {
    extend: {
      colors: {
        bg: "rgb(var(--color-bg) / <alpha-value>)",
        fg: "rgb(var(--color-fg) / <alpha-value>)",
        primary: "rgb(var(--color-primary) / <alpha-value>)",
      },
      borderRadius: {
        sm: "var(--radius-sm)",
        md: "var(--radius-md)",
      },
      spacing: {
        1: "var(--space-1)",
        2: "var(--space-2)",
        3: "var(--space-3)",
      },
    },
  },
};

Objavite tokens.css i Tailwind preset iz @acme/tokens. Vaša product app i @acme/ui oba ovise o njemu.

💡 Savjet: Držite površinu tokena malom. Većini timova treba manje od 10 semantičkih tokena boja po temi da bi se brzo kretali bez stalnih rasprava o tokenima.

Učitavanje tokena u Next.js#

U apps/web/src/app/layout.tsx importajte kompajlirani CSS iz tokena:

TSX
import "@acme/tokens/tokens.css";
import "./globals.css";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" data-theme="light">
      <body className="bg-bg text-fg">{children}</body>
    </html>
  );
}

Postavite data-theme prema korisničkoj preferenciji ili sistemskoj postavci, ali ga držite determinističkim za SSR.

# Korak 2: Gradite komponente na Radix UI primitivima#

Radix UI rješava teške dijelove: focus trapove, navigaciju tipkovnicom, ARIA atribute i ponašanje tipke Escape. Tailwind rješava vizualni sloj. Vaš dizajn sustav treba wrapati Radix u konzistentan API.

Konvencije komponenti koje se skaliraju#

Definirajte nekoliko pravila i provodite ih:

  • Exportajte isključivo iz jedne ulazne točke, npr. @acme/ui.
  • Svaka mapa komponente uključuje index.ts, *.tsx i *.stories.tsx.
  • Koristite cn helper za spajanje Tailwind klasa.
  • Ne izlažite Radix internals osim kad je potrebno.

Primjer cn helpera u packages/ui/src/lib/cn.ts:

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

Primjer komponente: Button#

Button postavlja baseline za sve komponente: veličine, stanja, focus ring i ponašanje disabled stanja.

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

Ovo vam daje:

  • Konzistentan focus ring kroz cijeli proizvod
  • Predvidljive tokenizirane veličine
  • Stroge varijante koje sprječavaju “one-off” stiliziranje

Primjer komponente: Dialog pomoću Radixa#

Dialog je mjesto gdje Radix odmah donosi vrijednost. Bugovi u upravljanju fokusom su skupi i često prođu neopaženo u produkciju.

TSX
import * as Dialog from "@radix-ui/react-dialog";
import { cn } from "../lib/cn";
 
export function Modal({
  title,
  children,
  open,
  onOpenChange,
}: {
  title: string;
  children: React.ReactNode;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}) {
  return (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50" />
        <Dialog.Content
          className={cn(
            "fixed left-1/2 top-1/2 w-[90vw] max-w-lg -translate-x-1/2 -translate-y-1/2",
            "rounded-md bg-bg p-6 shadow-lg focus-visible:outline-none"
          )}
        >
          <Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
          <div className="mt-3">{children}</div>
          <Dialog.Close className="mt-6 inline-flex h-10 px-4 items-center rounded-md bg-fg/10">
            Close
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

⚠️ Upozorenje: Nemojte uklanjati Radix zadana ponašanja poput focus trappinga ili escape-to-close kako biste uskladili “pixel-perfect” prototip. Ta ponašanja su zahtjevi pristupačnosti, a njihovo uklanjanje povećava troškove podrške i pravni rizik.

# Korak 3: Zadane postavke pristupačnosti koje ne ovise o disciplini developera#

Većina regresija pristupačnosti dolazi iz nedosljednih defaulta: nedostajuće labele, pokvareni focus stilovi i pogrešna semantika. Dizajn sustav treba smanjiti broj odluka koje developeri moraju donositi.

A11y checklist ugrađen u komponente#

Područje komponenteDefaultZašto je važno
Focus stilovifocus-visible:ring-2Korisnicima tipkovnice treba jasan indikator fokusa
LabeleZahtijevajte aria-label kad nema vidljive labeleScreen readerima trebaju pristupačna imena
Disabled stanjedisabled:pointer-events-none plus opacitySprječava slučajne klikove i razjašnjava stanje
Hit targetiMinimalna visina h-10 za gumbePoboljšava upotrebljivost na mobitelu
Dijalozi i izborniciRadix primitiveIspravan roving tabindex i upravljanje fokusom

Praktično pravilo: “Nema icon-only gumba bez labele”#

Olakšajte to tako da ponudite IconButton komponentu koja zahtijeva label prop.

TSX
import * as React from "react";
import { Button } from "./button";
 
export function IconButton(
  props: Omit<React.ComponentProps<typeof Button>, "children"> & {
    label: string;
    icon: React.ReactNode;
  }
) {
  const { label, icon, ...rest } = props;
  return (
    <Button aria-label={label} {...rest}>
      {icon}
    </Button>
  );
}

Ovo sprječava jedan od najčešćih pristupačnih “failova” u produkcijskim sučeljima.

# Korak 4: Storybook kao dokumentacija, playground i test runner#

Storybook je mjesto gdje dizajn sustav postaje upotrebljiv kroz timove. Smanjuje pitanja na Slacku, ubrzava review i djeluje kao UI test harness.

Osnove Storybook postavki#

Instalirajte dependencije u Storybook host paketu, a zatim osigurajte da importate tokene.

U .storybook/preview.ts:

TypeScript
import "@acme/tokens/tokens.css";
import type { Preview } from "@storybook/react";
 
const preview: Preview = {
  parameters: {
    controls: { expanded: true },
    a11y: { test: "todo" },
  },
};
 
export default preview;

Dobra priča je primjer korištenja, a ne snapshot#

Priča bi trebala:

  • Prikazati varijante i veličine
  • Dati kontrole za ključne propove
  • Demonstrirati “stvaran” sadržaj, ne samo lorem ipsum

Primjer button.stories.tsx:

TSX
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button";
 
const meta: Meta<typeof Button> = {
  title: "Core/Button",
  component: Button,
  args: { children: "Continue", variant: "primary", size: "md" },
};
export default meta;
 
export const Primary: StoryObj<typeof Button> = {};
export const Secondary: StoryObj<typeof Button> = {
  args: { variant: "secondary" },
};
export const Disabled: StoryObj<typeof Button> = {
  args: { disabled: true },
};

ℹ️ Napomena: Za mnoge timove, Storybook zamjenjuje zasebnu web stranicu dizajn sustava. Ušteda vremena u onboardingu i UI reviewu često premaši početni trošak setupa unutar prvih 4 do 8 tjedana.

Storybook interaction testovi#

Interaction testovi hvataju probleme ponašanja poput “Escape zatvara modal” i “Tab ostaje unutar dijaloga”. Takve je probleme skupo loviti isključivo kroz manual QA.

Držite test malenim i fokusiranim:

TSX
import type { Meta, StoryObj } from "@storybook/react";
import { within, userEvent } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
import { Modal } from "./modal";
 
const meta: Meta<typeof Modal> = { title: "Overlays/Modal", component: Modal };
export default meta;
 
export const OpensAndCloses: StoryObj<typeof Modal> = {
  render: () => <Modal title="Title" open={true} onOpenChange={() => {}}>Body</Modal>,
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await expect(canvas.getByText("Title")).toBeInTheDocument();
  },
};

# Korak 5: Integrirajte dizajn sustav u Next.js product codebase#

Imate dva cilja integracije:

  1. 1
    Učiniti usvajanje jednostavnim
  2. 2
    Učiniti zaobilaženje teškim

Checklist za integraciju#

PodručjeŠto napravitiŠto sprječava
ImportiKoristite samo @acme/uiNasumične kopije komponenti
TokeniUčitajte @acme/tokens/tokens.css u root layoutuDrift boja između aplikacija
TailwindKoristite tokens preset u app configuHardkodirane vrijednosti u utilityjima
SSRDržite temu determinističnom na serveruHydration mismatch
MigracijaUI mijenjajte postupno“Big bang” rewrites koji zapnu

U apps/web/tailwind.config.ts, koristite preset:

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

Migracijski obrazac koji radi u stvarnim timovima#

Prvo zamijenite UI s velikim churnom:

  • Gumbi i linkovi
  • Form kontrole
  • Modali i dropdownovi
  • Tablice i prikaz podataka

Ovo brže smanjuje broj bugova jer se te komponente pojavljuju na mnogo ekrana i imaju puno stanja.

# Korak 6: Nametnite konzistentnost lintanjem i granicama importa#

Konzistentnost se ne skalira samo kroz code review. Automatizirano provođenje je jeftinije i pouzdanije.

ESLint pravila za odvraćanje od zaobilaženja#

Koristite no-restricted-imports kako biste blokirali direktne Radix importe u product codeu, i time prisilili @acme/ui wrapere:

JavaScript
module.exports = {
  rules: {
    "no-restricted-imports": [
      "error",
      {
        paths: [
          {
            name: "@radix-ui/react-dialog",
            message: "Use Modal from @acme/ui instead of importing Radix directly.",
          },
        ],
      },
    ],
  },
};

Dodajte ograničenja za raw HTML elemente kad je prikladno. Primjerice, zabranite raw button u app codeu kako biste osigurali konzistentne focus stilove i varijante. Ovo primjenjujte pažljivo kako ne biste usporili legitimne slučajeve.

💡 Savjet: Neka pravilo bude opt-in po folderu. Prvo ga strogo provodite u apps/web/src/features, a dopustite iznimke u apps/web/src/playground ili migracijskim folderima.

Pravila konzistentnosti Tailwind klasa#

Ako dopustite proizvoljno korištenje Tailwinda, dobit ćete nekonzistentan UI. Cilj nije zabraniti Tailwind, nego ga ograničiti na odobrene tokene i obrasce.

Česte opcije provođenja:

  • Ograničiti određene klase poput text-red-500 u app codeu, i umjesto toga zahtijevati semantičke tokene.
  • Zahtijevati bg-bg, text-fg i semantičke boje umjesto palette boja.

Ovdje se pristup iz najboljih praksi za Tailwind CSS isplati: manje custom overrideova utilityja i manje “one-off” stringova klasa.

# Korak 7: Testiranje vizualne regresije i CI gateovi#

Unit testovi propuštaju UI probleme poput razmaka, boja i z-index slojevanja. Testovi vizualne regresije to pouzdano hvataju.

Dvije praktične opcije#

OpcijaTrud oko postavljanjaTrajni trošakNajbolje za
ChromaticNizakPlaćeno na skaliTimove koji žele brzinu i hostane diffove
Playwright screenshotoviSrednjiCompute vrijeme u CI-juTimove koji žele potpunu kontrolu i self-hosting

Realističan baseline je snapshotati:

  • Varijante gumba
  • Stanja form fieldova
  • Layering modala i popovera
  • Dark mode teme

Minimalni primjer Playwright screenshot testa#

Kreirajte story route i snapshotajte renderani story iframe. Neka bude jednostavno i malo po testu.

TypeScript
import { test, expect } from "@playwright/test";
 
test("Button primary visual", async ({ page }) => {
  await page.goto("http://localhost:6006/?path=/story/core-button--primary");
  await expect(page).toHaveScreenshot("button-primary.png", { fullPage: true });
});

Ovo odmah hvata promjene u paddingu, border radiusu i tokenima boja.

⚠️ Upozorenje: Vizualna regresija radi samo ako su okoline deterministične. Pinajte fontove, postavite konzistentan viewport i izbjegavajte time-based UI u pričama.

# Korak 8: Verzioniranje, izdanja i kompatibilnost unatrag#

Dizajn sustav koji se ne može sigurno isporučivati bit će ignoriran. Verzioniranje i changelogovi su ono što čini usvajanje predvidljivim.

Koristite semantičko verzioniranje s Changesets#

Praktična politika:

  • Patch izdanja: bug fixovi, interni refaktori
  • Minor izdanja: nove komponente, novi propovi s defaultima
  • Major izdanja: breaking API, preimenovanje tokena, promjena ponašanja

Checklist izdanja po promjeni:

  • Ažurirane priče
  • Ažurirana dokumentacija u Storybooku
  • Vizualni snapshotovi ažurirani i odobreni
  • Dodana Changeset datoteka

Primjer changeset datoteke:

Md
---
"@acme/ui": minor
---
 
Add secondary variant to Button and document usage in Storybook.

Strategija depreciranja za stvarne proizvode#

Breaking promjene stvaraju churn. Preferirajte deprecacije s vremenskim okvirom:

  • Ostavite stari prop da radi 1 do 2 minor verzije
  • Dodajte lint pravilo upozorenja za deprecated korištenje
  • Osigurajte codemodove za mehaničke migracije

Ako se dizajn sustav koristi kroz više aplikacija, ovaj pristup može uštedjeti dane rada po kvartalu.

# Ključni zaključci#

  • Definirajte tokene kao semantičke CSS varijable i mapirajte Tailwind na njih kako bi teme i utilityji ostali konzistentni.
  • Wrapajte Radix UI primitive vlastitim API-jem kako biste standardizirali pristupačnost, focus stilove i varijante.
  • Koristite Storybook kao dokumentaciju i test harness s a11y provjerama i interaction testovima, ne samo kao galeriju.
  • Integrirajte kroz workspace pakete i Tailwind presete, zatim prvo migrirajte UI s najvećim churnom kako biste brzo dobili ROI.
  • Nametnite korištenje lint pravilima i granicama importa, a UI drift hvatajte vizualnom regresijom u CI-ju.
  • Verzionirajte dizajn sustav s Changesets i semantičkim verzioniranjem, uz deprecacije kako ne biste razbijali produkt timove.

# Zaključak#

Produkcijski spreman dizajn sustav u Next.js je pipeline: tokeni pogone Tailwind, Radix daje pristupačno ponašanje, Storybook dokumentira i testira, a CI provodi konzistentnost kroz lintanje i vizualnu regresiju.

Ako želite pomoć u dizajnu modela tokena, izradi komponenti baziranih na Radixu ili postavljanju Storybooka plus CI gateova za Next.js proizvod, kontaktirajte Samioda i možemo implementirati verzionirani dizajn sustav koji vaš tim može isporučivati s pouzdanjem.

FAQ

Share
A
Adrijan OmićevićOsnivač i senior developer

Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.

Trebate pomoć s projektom?

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