Design

Дизайн-системи: як створити масштабовану UI-бібліотеку

cyberwolf.studio
Design/10 min read/January 10, 2025
Vladyslav Gaysyuk
Vladyslav Gaysyuk
Founder & CEO

#Дизайн-системи: як створити масштабовану UI-бібліотеку

Дизайн-система — це не просто набір кнопок і полів вводу. Це єдине джерело правди для всього візуального і інтерактивного в продукті. У CyberWolf.Studio ми створили десятки дизайн-систем для продуктів різного масштабу, і ділимося нашим підходом.

##Навіщо потрібна дизайн-система

Без системного підходу UI неминуче деградує:

  • 12 відтінків сірого замість структурованої палітри
  • 5 різних кнопок для однакової дії
  • Різна типографіка на кожній сторінці
  • Неконсистентні відступи між елементами

Дизайн-система вирішує ці проблеми системно, забезпечуючи спільну мову між дизайнерами та розробниками.

##Крок 1: Design Tokens

Токени — фундамент системи. Це абстрактний шар між дизайн-рішеннями та їх реалізацією:

css
1/* Семантичні CSS-змінні */
2:root {
3 /* Кольори */
4 --color-background: 0 0% 100%;
5 --color-foreground: 222 47% 11%;
6 --color-accent: 262 83% 58%;
7 --color-muted: 215 16% 47%;
8 --color-border: 214 32% 91%;
9 --color-destructive: 0 84% 60%;
10 --color-success: 142 71% 45%;
11
12 /* Типографіка */
13 --font-sans: "Inter", system-ui, sans-serif;
14 --font-mono: "JetBrains Mono", monospace;
15
16 /* Відступи (базовий крок: 4px) */
17 --space-1: 0.25rem;
18 --space-2: 0.5rem;
19 --space-3: 0.75rem;
20 --space-4: 1rem;
21 --space-6: 1.5rem;
22 --space-8: 2rem;
23 --space-12: 3rem;
24 --space-16: 4rem;
25
26 /* Радіуси */
27 --radius-sm: 0.25rem;
28 --radius-md: 0.5rem;
29 --radius-lg: 0.75rem;
30 --radius-full: 9999px;
31
32 /* Тіні */
33 --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
34 --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
35 --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
36}
37
38/* Темна тема — перевизначення токенів */
39[data-theme="dark"] {
40 --color-background: 222 47% 11%;
41 --color-foreground: 210 40% 98%;
42 --color-border: 217 33% 17%;
43}

##Крок 2: Базові компоненти

Будуємо знизу вгору — від найпростіших до складних:

###Кнопка — найчастіше використовуваний компонент

tsx
1import { cva, type VariantProps } from "class-variance-authority"
2
3const buttonVariants = cva(
4 // Базові стилі
5 "inline-flex items-center justify-center font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
6 {
7 variants: {
8 variant: {
9 primary: "bg-accent text-white hover:bg-accent/90",
10 secondary: "border border-border bg-transparent hover:bg-muted/10",
11 ghost: "hover:bg-muted/10",
12 destructive: "bg-destructive text-white hover:bg-destructive/90",
13 link: "text-accent underline-offset-4 hover:underline",
14 },
15 size: {
16 sm: "h-8 px-3 text-xs",
17 md: "h-10 px-4 text-sm",
18 lg: "h-12 px-6 text-base",
19 icon: "h-10 w-10",
20 },
21 },
22 defaultVariants: {
23 variant: "primary",
24 size: "md",
25 },
26 }
27)
28
29interface ButtonProps
30 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
31 VariantProps<typeof buttonVariants> {
32 loading?: boolean
33}
34
35export function Button({ variant, size, loading, children, ...props }: ButtonProps) {
36 return (
37 <button className={buttonVariants({ variant, size })} disabled={loading} {...props}>
38 {loading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
39 {children}
40 </button>
41 )
42}

###Input — з вбудованою підтримкою станів

tsx
1interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
2 label: string
3 error?: string
4 hint?: string
5}
6
7export function Input({ label, error, hint, id, ...props }: InputProps) {
8 const inputId = id || label.toLowerCase().replace(/\s+/g, "-")
9
10 return (
11 <div className="space-y-1.5">
12 <label htmlFor={inputId} className="text-sm font-medium text-foreground">
13 {label}
14 </label>
15 <input
16 id={inputId}
17 className={cn(
18 "h-10 w-full border bg-background px-3 text-sm transition-colors",
19 "placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent",
20 error ? "border-destructive" : "border-border"
21 )}
22 aria-invalid={!!error}
23 aria-describedby={error ? `${inputId}-error` : undefined}
24 {...props}
25 />
26 {error && (
27 <p id={`${inputId}-error`} className="text-xs text-destructive">
28 {error}
29 </p>
30 )}
31 {hint && !error && (
32 <p className="text-xs text-muted">{hint}</p>
33 )}
34 </div>
35 )
36}

##Крок 3: Складені компоненти

Комбінуємо прості компоненти у більш складні патерни:

РівеньПрикладиХарактеристика
АтомиButton, Input, Badge, AvatarОдин HTML-елемент або мінімальна композиція
МолекулиFormField, SearchBar, MenuItemКомбінація 2-3 атомів
ОрганізмиDataTable, NavigationBar, CardСамостійні секції інтерфейсу
ШаблониDashboardLayout, AuthLayoutСтруктура цілої сторінки

##Крок 4: Забезпечення якості

###Accessibility (a11y)

Кожен компонент перевіряється на доступність:

  • Коректна семантика HTML
  • Підтримка клавіатурної навігації
  • ARIA-атрибути де потрібно
  • Контрастність кольорів (WCAG 2.1 AA)

###Візуальне тестування

typescript
1// Chromatic для автоматичного visual regression testing
2// Кожен PR автоматично перевіряється на візуальні зміни
3test("Button renders correctly", async ({ page }) => {
4 await page.goto("/storybook/?path=/story/button--primary")
5 await expect(page.locator(".button")).toHaveScreenshot("button-primary.png")
6})
"

"Дизайн-система — це не проєкт з дедлайном. Це процес, який живе разом з продуктом."

##Підсумки

Створення дизайн-системи — це інвестиція, яка окупається з кожним новим компонентом і кожною новою сторінкою. Починайте з малого — токени, кілька базових компонентів, документація. Потім рости органічно, додаючи те, що реально потрібно продукту.

***

Потрібна дизайн-система для вашого продукту? Зв'яжіться з нами — створимо її разом.

Design SystemUI/UXКомпонентиFigma