Как делать11 октября 2026 г.9 мин чтения
UX/UI-дизайн: от исследования до дизайн-системыШаг 8 из 9

Дизайн-система на практике: токены, компоненты, правила

Коротко

Дизайн-система — это общий для дизайна и кода набор именованных решений (токенов), компонентов и правил их использования. Практичный старт — два слоя токенов: базовые хранят палитру и шкалы, семантические называют роли («акцент», «текст второго плана») и переключаются в тёмной теме. Токены записывают в JSON по формату W3C DTCG и генерируют из него CSS-переменные, которые читают компоненты.

Пошаговый урокУровень: СреднийДизайн-системыПрактика: ~1,5–3 ч

После материала вы сможете

  • описать цвет, типографику, отступы, радиусы и движение токенами в формате DTCG 2025.10 в два слоя — базовый и семантический
  • собрать из JSON файл с CSS custom properties и убедиться, что битая ссылка останавливает сборку
  • сделать компонент без единого жёсткого значения и переключить его в тёмную тему правкой только семантического слоя
  • сопоставить слои токенов с коллекциями и режимами Figma Variables

Проверено на

Design Tokens Format Module 2025.10 (W3C DTCG)Node.js 24.18Microsoft Edge 155 (Playwright 1.64)

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Что входит в дизайн-систему и когда она окупается
  2. 02Токены: базовые и семантические
  3. 03Шаг 1. Записываем токены в формате W3C DTCG
  4. 04Шаг 2. Переводим JSON в CSS custom properties
  5. 05Шаг 3. Компонент, который знает только токены
  6. 06Тёмная тема через семантические токены
  7. 07Документация, версии и минимальный набор

Что входит в дизайн-систему и когда она окупается

Nielsen Norman Group определяет дизайн-систему как полный набор стандартов, который помогает управлять дизайном в масштабе за счёт переиспользуемых компонентов и паттернов. На практике это не «файл в Figma с кнопками», а договорённость между дизайном и кодом: у каждого решения — цвета, отступа, радиуса, длительности анимации — есть имя, а у каждого компонента — правила, когда и как его применять.

Стиль-гайд

Что в ней
Бренд, тон текста, визуальные и интерактивные стандарты, принципы
Пример
«Кнопки называем глаголом: „Записаться“, а не „ОК“»

Библиотека компонентов

Что в ней
Переиспользуемые элементы с названиями, описанием, атрибутами, состояниями и кодом
Пример
Кнопка: основная, вторичная, наведение, фокус

Библиотека паттернов

Что в ней
Группы элементов и шаблоны экранов
Пример
Форма записи, карточка услуги в каталоге

Токены

Что в ней
Именованные значения, из которых собраны все три части
Пример
color.accent, space.md, radius.lg
Части дизайн-системы: первые три — по NN/g, токены — термин Material Design и W3C DTCG.

Когда это окупается. По оценке NN/g, дизайн-система лучше всего работает, когда компания видит впереди годы повторяемой дизайн-работы, а для раннего прототипа полная система вряд ли быстро окупится; к тому же её создание и поддержка требуют выделенной команды. Material Design даёт похожий фильтр для токенов: они мало помогут приложению с жёстко прописанными значениями, которое не собираются менять в ближайшие год-два.

Токены: базовые и семантические

Material Design называет токены маленькими переиспользуемыми дизайн-решениями: вместо значения #E8DEF8 — понятное имя вроде md.ref.palette.secondary90. Если макет дизайнера и код разработчика ссылаются на одно имя, они гарантированно используют один цвет, даже когда его значение поменяют.

Минимальная рабочая схема — два слоя. Базовые токены (у Material — reference) хранят все допустимые значения и ничего не знают о том, где они используются: base.color.brand-600, base.color.neutral-50. Семантические (у Material — system) описывают роль в интерфейсе: color.accent, color.text-muted, color.surface. Именно семантический слой переключается в тёмной теме, а Material подчёркивает: здесь и происходит темизация. Третий класс у Material — компонентные токены (md.comp…), которые указывают на системные; в нашем примере этот слой живёт прямо в CSS компонента.

  1. 01Базовыеbase.color.brand-600 = #3d4fd6 — палитра и шкалы, без смысла
  2. 02Семантическиеcolor.accent → base.color.brand-600 — роль; меняется по теме
  3. 03Компонентные--button-bg → color.accent — свойства одного компонента
  4. 04Компонент.button читает только свои переменные
Каждый слой ссылается только на соседний ниже. Смена темы — правка семантического слоя, смена бренда — базового.

Имя по внешнему виду

  • --blue, --gray-light, --padding-16
  • Бренд стал зелёным — переменная --blue теперь зелёная
  • Непонятно, можно ли брать --gray-light для текста
  • Тёмная тема требует новых переменных и правки компонентов

Имя по роли

  • --color-accent, --color-text-muted, --space-md
  • Смена бренда — одна ссылка в семантическом слое
  • Роль подсказывает, где токен уместен
  • Тёмная тема — другой набор значений тех же имён

Тот же принцип — в Apple Human Interface Guidelines: динамические системные цвета определены по назначению, а не по внешнему виду, и Apple просит не переопределять их смысл — например, не использовать цвет разделителя как цвет текста. Это правило стоит перенести и на свои семантические токены.

Шаг 1. Записываем токены в формате W3C DTCG

Design Tokens Community Group при W3C 28 октября 2025 года опубликовала первую стабильную версию формата — Design Tokens Format Module 2025.10. Это отчёт сообщества (Final Community Group Report): документ считается стабильным, но не является стандартом W3C. Его ценность в том, что один и тот же JSON могут читать разные инструменты — плагины Figma, генераторы кода, документация.

  • Файл — обычный JSON, рекомендуемые расширения .tokens и .tokens.json
  • Токен — объект со свойством $value; объект без $value — группа
  • $type можно задать группе — его унаследуют все токены внутри
  • Ссылка на другой токен записывается в фигурных скобках: "{base.color.brand-600}"
  • Имена не начинаются с $ и не содержат точку и фигурные скобки — точка служит разделителем пути
  • $description — пояснение, $deprecated — пометка устаревшего токена
tokens/tokens.json
{
  "base": {
    "color": {
      "$type": "color",
      "neutral-0": { "$value": { "colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff" } },
      "neutral-50": { "$value": { "colorSpace": "srgb", "components": [0.961, 0.965, 0.973], "hex": "#f5f6f8" } },
      "neutral-200": { "$value": { "colorSpace": "srgb", "components": [0.867, 0.882, 0.906], "hex": "#dde1e7" } },
      "neutral-300": { "$value": { "colorSpace": "srgb", "components": [0.769, 0.792, 0.827], "hex": "#c4cad3" } },
      "neutral-600": { "$value": { "colorSpace": "srgb", "components": [0.294, 0.333, 0.388], "hex": "#4b5563" } },
      "neutral-800": { "$value": { "colorSpace": "srgb", "components": [0.122, 0.149, 0.2], "hex": "#1f2633" } },
      "neutral-900": { "$value": { "colorSpace": "srgb", "components": [0.078, 0.102, 0.141], "hex": "#141a24" } },
      "neutral-950": { "$value": { "colorSpace": "srgb", "components": [0.047, 0.063, 0.094], "hex": "#0c1018" } },
      "brand-200": { "$value": { "colorSpace": "srgb", "components": [0.78, 0.804, 1], "hex": "#c7cdff" } },
      "brand-300": { "$value": { "colorSpace": "srgb", "components": [0.647, 0.69, 1], "hex": "#a5b0ff" } },
      "brand-600": { "$value": { "colorSpace": "srgb", "components": [0.239, 0.31, 0.839], "hex": "#3d4fd6" } },
      "brand-700": { "$value": { "colorSpace": "srgb", "components": [0.184, 0.247, 0.722], "hex": "#2f3fb8" } },
      "red-300": { "$value": { "colorSpace": "srgb", "components": [1, 0.608, 0.58], "hex": "#ff9b94" } },
      "red-600": { "$value": { "colorSpace": "srgb", "components": [0.769, 0.169, 0.169], "hex": "#c42b2b" } }
    }
  },
  "color": {
    "$type": "color",
    "$description": "Семантические цвета: компоненты берут только их",
    "bg": { "$value": "{base.color.neutral-0}" },
    "surface": { "$value": "{base.color.neutral-50}" },
    "border": { "$value": "{base.color.neutral-200}" },
    "text": { "$value": "{base.color.neutral-900}" },
    "text-muted": { "$value": "{base.color.neutral-600}" },
    "accent": { "$value": "{base.color.brand-600}" },
    "accent-hover": { "$value": "{base.color.brand-700}" },
    "on-accent": { "$value": "{base.color.neutral-0}" },
    "danger": { "$value": "{base.color.red-600}" }
  },
  "space": {
    "$type": "dimension",
    "xs": { "$value": { "value": 4, "unit": "px" } },
    "sm": { "$value": { "value": 8, "unit": "px" } },
    "md": { "$value": { "value": 16, "unit": "px" } },
    "lg": { "$value": { "value": 24, "unit": "px" } },
    "xl": { "$value": { "value": 32, "unit": "px" } }
  },
  "radius": {
    "$type": "dimension",
    "sm": { "$value": { "value": 6, "unit": "px" } },
    "md": { "$value": { "value": 10, "unit": "px" } },
    "lg": { "$value": { "value": 16, "unit": "px" } }
  },
  "font": {
    "family": {
      "$type": "fontFamily",
      "sans": { "$value": ["Inter", "Segoe UI", "Arial", "sans-serif"] }
    },
    "size": {
      "$type": "dimension",
      "body": { "$value": { "value": 1, "unit": "rem" } },
      "small": { "$value": { "value": 0.875, "unit": "rem" } },
      "title": { "$value": { "value": 1.25, "unit": "rem" } }
    },
    "weight": {
      "$type": "fontWeight",
      "regular": { "$value": 400 },
      "bold": { "$value": 600 }
    },
    "line-height": {
      "$type": "number",
      "body": { "$value": 1.5 }
    }
  },
  "motion": {
    "duration": {
      "$type": "duration",
      "fast": { "$value": { "value": 150, "unit": "ms" } }
    },
    "easing": {
      "$type": "cubicBezier",
      "standard": { "$value": [0.2, 0, 0, 1] }
    }
  }
}
Базовые цвета, семантические роли, отступы, радиусы, типографика и движение. Файл проверен парсером JSON и сборкой из следующего шага.

color

Формат $value
Объект: colorSpace, components (0–1), необязательные alpha и hex — запасное значение
Пример
{ "colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff" }

dimension

Формат $value
Объект value + unit, единица только px или rem
Пример
{ "value": 16, "unit": "px" }

fontFamily

Формат $value
Строка или массив имён в порядке приоритета
Пример
["Inter", "Segoe UI", "Arial", "sans-serif"]

fontWeight

Формат $value
Число от 1 до 1000 или псевдоним: regular, semi-bold, bold…
Пример
600

number

Формат $value
Число JSON
Пример
1.5 — межстрочный интервал

duration

Формат $value
Объект value + unit, единица ms или s
Пример
{ "value": 150, "unit": "ms" }

cubicBezier

Формат $value
Четыре числа [x1, y1, x2, y2], x — от 0 до 1
Пример
[0.2, 0, 0, 1]
Типы DTCG 2025.10, которые использованы в файле.

Шаг 2. Переводим JSON в CSS custom properties

Браузер не читает JSON токенов — ему нужны CSS custom properties, их ещё называют CSS-переменными. Как описывает MDN, имя такого свойства начинается с двух дефисов, значение читается функцией var(), а объявление на :root делает переменную доступной всей странице. Имена чувствительны к регистру: --color-text и --Color-text — разные переменные. Значение наследуется потомками.

Скрипт ниже работает в Node.js 24 без зависимостей. Он обходит группы, наследует $type от группы, превращает ссылки в var() и проверяет, что каждая ссылка ведёт на существующий токен.

build-tokens.mjs
// Node.js 24, без зависимостей: tokens/*.json (формат DTCG 2025.10) → tokens.css
import { readFileSync, writeFileSync } from "node:fs";

const read = (file) => JSON.parse(readFileSync(file, "utf8"));
const GENERIC_FONTS = new Set(["serif", "sans-serif", "monospace", "system-ui", "cursive"]);

// путь в JSON → имя CSS-переменной: ["color", "text-muted"] → "--color-text-muted"
const cssName = (path) => `--${path.join("-")}`;

// "{base.color.neutral-0}" → "var(--base-color-neutral-0)"
function alias(value) {
  if (typeof value !== "string" || !/^\{[^{}]+\}$/.test(value)) return null;
  return `var(${cssName(value.slice(1, -1).split("."))})`;
}

function toCss(type, value) {
  const ref = alias(value);
  if (ref) return ref;
  switch (type) {
    case "color": {
      const alpha = value.alpha ?? 1;
      if (value.hex && alpha === 1) return value.hex;
      return `color(${value.colorSpace} ${value.components.join(" ")} / ${alpha})`;
    }
    case "dimension":
    case "duration":
      return `${value.value}${value.unit}`;
    case "fontFamily":
      return [value].flat().map((f) => (GENERIC_FONTS.has(f) ? f : `"${f}"`)).join(", ");
    case "fontWeight":
    case "number":
      return String(value);
    case "cubicBezier":
      return `cubic-bezier(${value.join(", ")})`;
    default:
      throw new Error(`Тип «${type}» не поддержан`);
  }
}

// обходит группы; $type наследуется от ближайшей группы
function collect(node, path = [], type, out = new Map()) {
  for (const [key, child] of Object.entries(node)) {
    if (key.startsWith("$")) continue;
    const childType = child.$type ?? type;
    if ("$value" in child) out.set(cssName([...path, key]), toCss(childType, child.$value));
    else collect(child, [...path, key], childType, out);
  }
  return out;
}

const light = collect(read("tokens/tokens.json"));
const dark = collect(read("tokens/tokens.dark.json"));

// каждая ссылка должна вести на существующий токен
for (const [name, value] of [...light, ...dark]) {
  const target = value.match(/^var\((--[^)]+)\)$/)?.[1];
  if (target && !light.has(target)) throw new Error(`${name}: нет токена ${target}`);
}

const block = (vars, pad = "  ") => [...vars].map(([n, v]) => `${pad}${n}: ${v};`).join("\n");
const css = `/* Сгенерировано build-tokens.mjs — не править вручную */
:root {
${block(light)}
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
${block(dark, "    ")}
  }
}

:root[data-theme="dark"] {
${block(dark)}
}
`;

writeFileSync("tokens.css", css);
console.log(`tokens.css: ${light.size} токенов, тёмная тема переопределяет ${dark.size}`);
Учебный конвертер: поддерживает только типы из нашего файла. Неизвестный тип — явная ошибка, а не молчаливый пропуск.
terminal
$ node build-tokens.mjs
tokens.css: 40 токенов, тёмная тема переопределяет 9
Запуск из корня папки с токенами.
tokens.css
/* Сгенерировано build-tokens.mjs — не править вручную */
:root {
  --base-color-neutral-0: #ffffff;
  /* … базовые токены … */
  --color-bg: var(--base-color-neutral-0);
  --color-surface: var(--base-color-neutral-50);
  --color-border: var(--base-color-neutral-200);
  --color-text: var(--base-color-neutral-900);
  --color-text-muted: var(--base-color-neutral-600);
  --color-accent: var(--base-color-brand-600);
  --color-accent-hover: var(--base-color-brand-700);
  --color-on-accent: var(--base-color-neutral-0);
  --color-danger: var(--base-color-red-600);
  /* … отступы, радиусы, шрифт, движение … */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: var(--base-color-neutral-950);
    --color-surface: var(--base-color-neutral-900);
    /* … остальные семантические цвета … */
  }
}

/* … и тот же набор в :root[data-theme="dark"] */
Фрагмент сгенерированного файла: семантические токены ссылаются на базовые через var().
  1. Читает два файла

    tokens.json — все токены и светлые значения ролей, tokens.dark.json — только роли, которые меняются в тёмной теме.

  2. Строит имена

    Путь в JSON склеивается через дефис: color → text-muted превращается в --color-text-muted.

  3. Сохраняет ссылки

    "{base.color.brand-600}" становится var(--base-color-brand-600) — связь слоёв остаётся видна в коде и в DevTools.

  4. Проверяет целостность

    Мы подставили в тёмную тему несуществующий neutral-1000 — сборка остановилась с ошибкой, а не выпустила CSS с пустой переменной.

  5. Пишет три блока

    :root со светлыми значениями, медиазапрос для системной тёмной темы и :root[data-theme="dark"] для выбора вручную.

Шаг 3. Компонент, который знает только токены

В стилях компонента нет ни одного hex-значения и ни одного «магического» числа: цвета, отступы, радиусы, шрифт и длительность перехода приходят из токенов. У кнопки свой маленький слой — --button-bg, --button-fg, --button-border. Вторичный вариант и наведение меняют только эти три переменные, а не переписывают свойства заново, поэтому новый вариант — это три строки.

components.css
body {
  margin: 0;
  padding: var(--space-xl);
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-family-sans);
  font-size: var(--font-size-body);
  line-height: var(--font-line-height-body);
}

.card {
  max-width: 360px;
  padding: var(--space-lg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.card__title {
  margin: 0 0 var(--space-sm);
  font-size: var(--font-size-title);
  font-weight: var(--font-weight-bold);
}

.card__text {
  margin: 0 0 var(--space-md);
  color: var(--color-text-muted);
}

.card__actions {
  display: flex;
  gap: var(--space-sm);
}

/* компонентные токены: кнопка читает только свои переменные */
.button {
  --button-bg: var(--color-accent);
  --button-fg: var(--color-on-accent);
  --button-border: var(--color-accent);

  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-md);
  border: 1px solid var(--button-border);
  border-radius: var(--radius-md);
  background: var(--button-bg);
  color: var(--button-fg);
  font: inherit;
  font-weight: var(--font-weight-bold);
  cursor: pointer;
  transition: background-color var(--motion-duration-fast) var(--motion-easing-standard);
}

.button:hover {
  --button-bg: var(--color-accent-hover);
  --button-border: var(--color-accent-hover);
}

.button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.button--secondary,
.button--secondary:hover {
  --button-bg: transparent;
  --button-fg: var(--color-accent);
  --button-border: var(--color-border);
}

.button--secondary:hover {
  --button-bg: var(--color-bg);
}
Карточка и кнопка на токенах. Минимальная высота 44px — запас для нажатия пальцем.
index.html
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Токены в деле</title>
    <link rel="stylesheet" href="tokens.css" />
    <link rel="stylesheet" href="components.css" />
  </head>
  <body>
    <article class="card">
      <h2 class="card__title">Запись на консультацию</h2>
      <p class="card__text">Все цвета, отступы и радиусы этой карточки берутся из токенов.</p>
      <div class="card__actions">
        <button class="button" type="button">Записаться</button>
        <button class="button button--secondary" type="button">Подробнее</button>
      </div>
    </article>
  </body>
</html>
Страница для проверки: подключает сгенерированный tokens.css и стили компонентов.

Как проверить результат. Запустите node build-tokens.mjs, откройте index.html и в DevTools посмотрите вкладку Computed у основной кнопки: background-color должен быть rgb(61, 79, 214) — значение base.color.brand-600, прошедшее через color.accent и --button-bg. Мы прогнали эту страницу в headless Microsoft Edge через Playwright: светлая и тёмная системная тема, ручной data-theme в обе стороны, наведение и проверка ссылок — все 9 автоматических проверок прошли.

Тёмная тема через семантические токены

Тёмная тема — это второй набор значений тех же семантических токенов. Базовая палитра и компоненты не меняются: файл ниже переопределяет девять ролей и больше ничего.

tokens/tokens.dark.json
{
  "color": {
    "$type": "color",
    "bg": { "$value": "{base.color.neutral-950}" },
    "surface": { "$value": "{base.color.neutral-900}" },
    "border": { "$value": "{base.color.neutral-800}" },
    "text": { "$value": "{base.color.neutral-50}" },
    "text-muted": { "$value": "{base.color.neutral-300}" },
    "accent": { "$value": "{base.color.brand-300}" },
    "accent-hover": { "$value": "{base.color.brand-200}" },
    "on-accent": { "$value": "{base.color.neutral-950}" },
    "danger": { "$value": "{base.color.red-300}" }
  }
}

В tokens.css тёмные значения попадают дважды. Медиазапрос prefers-color-scheme: dark с селектором :root:not([data-theme="light"]) следует системной настройке, если человек не выбрал светлую тему явно. Блок :root[data-theme="dark"] включает тёмную тему по выбору — достаточно выполнить document.documentElement.dataset.theme = "dark" в обработчике переключателя.

text / bg

Светлая тема
17,46:1
Тёмная тема
17,61:1

text-muted / surface

Светлая тема
6,99:1
Тёмная тема
10,59:1

on-accent / accent

Светлая тема
6,39:1
Тёмная тема
9,30:1

accent / surface

Светлая тема
5,91:1
Тёмная тема
8,52:1

danger / bg

Светлая тема
5,63:1
Тёмная тема
9,40:1
Контраст семантических пар нашей палитры, посчитанный скриптом проверки по формуле WCAG. Минимум для обычного текста по критерию 1.4.3 — 4,5:1.

В Figma та же структура собирается из переменных. По справке Figma, коллекция — это набор переменных и режимов; режим хранит по одному значению каждой переменной для своего контекста — светлой и тёмной темы, языка, размера устройства. Переменная может ссылаться на другую переменную того же типа — это и есть алиас. Типы: цвет, число, строка, логическое значение, а для Figma Motion ещё длительность и easing. Удобное соответствие: коллекция «Base» без режимов повторяет группу base, коллекция «Semantic» с режимами Light и Dark — файлы tokens.json и tokens.dark.json.

У DTCG для тем есть отдельный документ — Resolver Module 2025.10. Он описывает наборы токенов и модификаторы с контекстами (например, light и dark) в файле .resolver.json. Наша пара файлов и скрипт — упрощённая версия той же идеи; если ваш инструмент поддерживает resolver, описывайте темы через него.

Документация, версии и минимальный набор

Система, которую никто не ведёт, расходится с продуктом за несколько релизов. NN/g подчёркивает, что дизайн-система требует постоянной поддержки, и называет минимальную команду: дизайнер взаимодействия, визуальный дизайнер и разработчик. Документация при этом не обязана быть отдельным порталом: на старте хватает таблицы компонентов со статусами и ссылками на токены.

Кнопка

Статус
Готов
Токены
--button-*, color.accent, radius.md, motion.*
Состояния и правила
Основная и вторичная; наведение, фокус. Одна основная кнопка на экран

Карточка

Статус
Готов
Токены
color.surface, color.border, radius.lg, space.lg
Состояния и правила
Без тени в тёмной теме: глубину даёт цвет поверхности

Поле ввода

Статус
В работе
Токены
color.border, color.danger, radius.md
Состояния и правила
Фокус, ошибка с текстом, недоступно

Бейдж

Статус
Черновик
Токены
color.accent, font.size.small
Состояния и правила
Нужен токен радиуса-«пилюли»

Модальное окно

Статус
Не начато
Токены
—
Состояния и правила
Сначала описать фокус и закрытие с клавиатуры
Пример таблицы компонентов. Статус «готов» — компонент есть в Figma и в коде, состояния описаны.

Имена токенов для кода — такой же публичный интерфейс, как функции библиотеки. Semantic Versioning предписывает повышать мажорную версию при несовместимых изменениях API, минорную — при обратно совместимых добавлениях, патч — при исправлениях. Наш совет — переносить это на токены буквально: переименование или удаление токена — мажорная версия, новый токен — минорная, исправленное значение — патч. Перед удалением помечайте токен $deprecated с подсказкой, чем его заменить.

  • Базовая палитра: нейтральные, фирменный, цвета ошибки и успеха
  • 8–12 семантических цветов: фон, поверхность, граница, текст, текст второго плана, акцент, текст на акценте, ошибка
  • Шкала отступов с шагом 4/8 — подробнее в уроке о визуальной иерархии
  • 3–5 размеров шрифта и 2 начертания — из урока о типографике и цвете
  • 2–3 радиуса, 2–3 длительности и одна-две кривые easing
  • Кнопка, поле ввода и карточка со всеми состояниями
  • Таблица компонентов со статусами и правило версий
Как проверено6 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
  • запущено, работает

    tokens/tokens.json, tokens/tokens.dark.json

    JSON.parse в Node.js 24.18; components ↔ hex сверены скриптом (расхождение ≤ 0,002)

  • запущено, работает

    build-tokens.mjs → tokens.css

    node build-tokens.mjs: 40 токенов, 9 переопределений; негативный тест — несуществующая ссылка {base.color.neutral-1000} останавливает сборку; фрагмент tokens.css в статье взят из сгенерированного файла

  • запущено, работает

    components.css + index.html

    Playwright 1.64 (из node_modules репозитория), channel msedge, Edge 155 headless: getComputedStyle в светлой/тёмной prefers-color-scheme, data-theme=light/dark, hover (accent-hover), ловушка наследования (переопределение базового токена на .card не меняет фон); скриншоты shot-light.png / shot-dark.png; 9/9 проверок

  • запущено, работает

    контраст семантических пар

    формула относительной яркости WCAG в verify.mjs, все 9 пар ≥ 4,5:1 в обеих темах; числа таблицы — вывод скрипта

  • запущено, работает

    EN-версия кода (переведены комментарии, сообщения и тексты страницы)

    тот же verify.mjs в отдельной папке dizajn-sistema-en: 9/9

  • запущено, работает

    решения упражнений 1–2 (.badge с radius.pill, .button--danger с on-danger)

    копия проекта ds-ex: сборка 42 токена, контраст on-danger/danger ≥ 4,5:1 в обеих темах, computed: danger rgb(196,43,43)/rgb(255,155,148) при hover, badge radius 999px

  • не запускалось

    упражнение 3 (режим повышенной контрастности)

    решение дано текстом без кода; значения не проверялись

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.