Справочник утилит

Отступы, размеры, типографика, цвета, границы, скругления, тени, позиционирование, эффекты, видимость, интерактивность и переходы.

griffincss-utils полный список

Colors (scss/_colors.scss)

HSL-цветовая система: нейтральная шкала (gray) по золотому сечению + 6 акцентных цветов (72° шаг по кругу). Каждый акцент имеет -light (+15% L) и -dark (−15% L) варианты.

ГруппаКлассы
Фон.gr-bg-white, .gr-bg-gray-50.gr-bg-gray-900, .gr-bg-black, .gr-bg-primary.gr-bg-info-dark
Текст.gr-text-white, .gr-text-gray-50.gr-text-gray-900, .gr-text-black, .gr-text-primary.gr-text-info-dark. Это цвета палитры, а не роли: .gr-text-secondary — синий второго акцента, а не второстепенный текст; за ним — .gr-text-ink-secondary, за приглушённым — .gr-text-ink-muted
Рамка.gr-border-white, .gr-border-gray-50.gr-border-gray-900, .gr-border-black, .gr-border-primary.gr-border-info-dark

HSL-переменные: --gr-hsl-gray-*, --gr-hsl-primary, --gr-hsl-success и т.д. — пользователь переопределяет hue/saturation/lightness по отдельности. Семантические токены: --gr-color-bg, --gr-color-text, --gr-color-link и др. — ссылаются на HSL-переменные.

Классы выше — литеральные: .gr-bg-white остаётся белым в любой теме, и это контракт, а не недоработка. Разметку, которая должна жить по теме, пишут на семантических классах — они в таблице ниже, в разделе «Темы».

Темы: тёмная и для слабовидящих (scss/_theme-tokens.scss и _theme-rules.scss ядра)

Демонстрация: docs/theme.html.

Две независимые оси, обе — атрибутом на любом элементе, обычно на <html>:

АтрибутЗначенияЕсли атрибута нет
data-gr-themelight, dark, autoследует prefers-color-scheme
data-gr-a11ylow-vision, offследует prefers-contrast
<html data-gr-theme="dark" data-gr-a11y="low-vision">

Тема меняет только значения семантической шкалы — --gr-hsl-surface-*, --gr-hsl-ink-* и их соседей, — поэтому разметка при переключении не трогается. Селекторы написаны без привязки к корню, и тёмная секция внутри светлой страницы — это <section data-gr-theme="dark">.

Семантические утилиты:

ГруппаКлассы
Поверхности.gr-bg-page, .gr-bg-surface, .gr-bg-raised, .gr-bg-sunken, .gr-bg-accent
Текст.gr-text-ink, .gr-text-ink-secondary, .gr-text-ink-muted, .gr-text-ink-disabled, .gr-text-accent, .gr-text-on-accent, .gr-text-link. Роли текста живут здесь: второстепенный — .gr-text-ink-secondary, приглушённый — .gr-text-ink-muted; .gr-text-secondary из таблицы выше — цвет палитры
Границы.gr-border-subtle, .gr-border-strong

.gr-text-ink-disabled предназначен плейсхолдерам и неактивным элементам: он даёт около 4:1 и для текста не годится.

Режим для слабовидящих делится надвое. Контраст (чистые чёрный и белый, границы толще и темнее, ссылки до 7:1, кольцо фокуса шире) включается и системной настройкой prefers-contrast: more. Типографика (кегль ×1.25, интерлиньяж 1.7, трекинг, подчёркнутые ссылки) — только явным data-gr-a11y="low-vision": системное «больше контраста» не означает «увеличь шрифт».

Переключатель — отдельный рантайм, griffincss-core/dist/griffincss-theme.js. Без него тема тоже работает: страница просто следует системным настройкам.

<!-- Синхронно и в <head>: атрибут ставится до первой отрисовки,
     поэтому сохранённая тёмная тема не мигает светлой -->
<script src="griffincss-theme.js"></script>
Griffincss.theme.set('dark');       // 'light' | 'dark' | 'auto'
Griffincss.theme.toggle();          // светлая ⇄ тёмная
Griffincss.theme.a11y(true);        // true | false | 'auto'
Griffincss.theme.style('strict');   // 'standard' | 'airy' | 'strict' | 'compact'
Griffincss.theme.get();             // {theme, resolved, style, a11y, resolvedA11y, version}

document.addEventListener('griffincss:themechange', function (e) {
  console.log(e.detail.resolved);   // 'light' | 'dark'
});

Выбор запоминается в localStorage (ключи gr-theme, gr-style и gr-a11y); data-persist="false" на теге скрипта это отключает, data-auto="false" — запрещает трогать документ при загрузке. Событие смены системной настройки приходит только тогда, когда выбор отдан системе.

Стратегии оформления (scss/_styles.scss ядра)

Демонстрация: docs/style-presets.html.

Третья ось, независимая от двух предыдущих: она меняет не цвет, а геометрию, плотность и движение — и слегка сдвигает цвет относительно действующей темы.

ЗначениеНастроение
standardкак база: скруглённые границы, цветные кнопки
airyскругления крупнее, просторнее, пастельный акцент, отклик с лёгким перелётом
strictуглы почти прямые, тени до наметки, границы бледнее, фоны к краю шкалы
compactто же, что строгое, но плотнее: меньше зазоры, ниже интерлиньяж, разделители строк в таблице
<html data-gr-theme="dark" data-gr-style="strict" data-gr-a11y="low-vision">

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

<link rel="stylesheet" href="griffincss-core.css">
<link rel="stylesheet" href="griffincss-styles.css">

Файл лежит в собственном слое griffincss.style, последнем и старшем из библиотечных, поэтому порядок тегов <link> ничего не решает. Пользовательский CSS вне слоёв по-прежнему выигрывает у библиотеки без !important.

Ось управляет ручками, а компоненты читают их через var():

РучкаРоль
--gr-radius, --gr-radius-pillскругление обычное и таблеточное
--gr-densityмножитель плотности: зазоры, высоты и внутренние отступы элементов управления
--gr-elevationмножитель высоты теней — отдельно от --gr-shadow-strength, которую задаёт тема
--gr-border-width, --gr-transition, --gr-leading-baseтолщина линий, длительность и кривая отклика, интерлиньяж

Утилиты отступов ось не двигает. .gr-p-6 остаётся 1.5rem в любом стиле: утилита — явное указание автора страницы, и стилю не место в нём. Плотность действует на собственные отступы компонентов и на --gr-gap.

Цвет сдвигается не абсолютными значениями, а якорями, которые задаёт действующая тема: один и тот же блок правил осветляет фоны на светлой теме и делает их графитовыми на тёмной.

У статуса три роли, и стиль трогает не все. Цвет текста (--gr-color-danger), цвет заливки (--gr-color-danger-surface) и цвет чернил на этой заливке (--gr-color-on-danger) — три отдельных токена у каждого из четырёх статусов. Стиль меняет заливку и чернила, а текстовый цвет оставляет тёмным: «опасность» в тексте на белом фоне пастельной быть не может. Воздушный смягчает заливки, журнальный приглушает их, строгий не трогает вовсе.

Атрибут работает на любом элементе, поэтому островок со своим оформлением — это <section data-gr-style="compact">. Островок возвращает геометрию, но цвет наследует от стиля страницы: восстановить затёртые триплеты стилю неоткуда, их задала тема. Полный сброс требует обеих осей — <section data-gr-style="standard" data-gr-theme="light">.

Приоритет. Тема задаёт цвет, стиль — геометрию и движение, режим для слабовидящих перебивает обоих: границы возвращаются к утолщённым, а журнальный стиль перестаёт ужимать интерфейс. Воздушный при этом простора не теряет — «крупнее и просторнее» режиму доступности не противоречит.

Состав файла настраивается одним списком, общим для ядра и компонентов:

@use 'griffincss-core/scss/style-config' with ($gr-styles: (strict));

@use 'griffincss-core/scss/griffincss-styles';
@use 'griffincss-ui';

Пустой список () выключает ось целиком. Неизвестное имя роняет сборку.

В scoped-сборке структурных правил нет. griffincss-ui-scoped.css ограничен областью .griffin, а атрибут стоит на <html>: селектор там получает префикс :where(.griffin) и требует носитель атрибута внутри области. Ось там действует на уровне токенов — скругления, плотность, тени и цвета работают полностью, — но шапка карточки не превращается в линию.

А вот порог браузеров у scoped-сборок тот же, что у обычных. :where() знают Chrome 88, Safari 14 и Firefox 78 — это ниже каскадных слоёв, на которых стоит вся библиотека. Цифры и метод замера — в разделе «Совместимость».

Направление письма / Writing direction

Утилиты, работающие вдоль строки, — логические: класс называет сторону строкиstart и end — и разворачивается сам, когда на элементе или предке стоит dir="rtl". Правил на [dir] в библиотеке нет и не нужно: логические свойства разворачивает браузер. Физические односторонние классы удалены; таблица соответствия для старой разметки:

Было (удалено)СталоСвойство
.gr-ml-{n} / .gr-mr-{n}.gr-ms-{n} / .gr-me-{n}margin-inline-start / -end
.gr-pl-{n} / .gr-pr-{n}.gr-ps-{n} / .gr-pe-{n}padding-inline-start / -end
.gr-ml-auto / .gr-mr-auto.gr-ms-auto / .gr-me-automargin-inline-start / -end: auto
.gr-text-left / .gr-text-right.gr-text-start / .gr-text-endtext-align: start / end
.gr-left-0 / .gr-right-0.gr-start-0 / .gr-end-0inset-inline-start / -end
.gr-border-l / .gr-border-r.gr-border-s / .gr-border-eborder-inline-start / -end

Осевые классы — .gr-mx-{n}, .gr-px-{n}, .gr-inset-x-0, .gr-border-x и их вертикальные пары — записаны логическими сокращениями (margin-inline, inset-inline, border-inline, border-block). Обе стороны у них получают одно значение, поэтому направление письма результата не меняет, а объявлений выводится вдвое меньше.

Почему удалены, а не оставлены рядом. Пара из физического и логического класса на каждую сторону удваивала селекторы, а разметка на .gr-ml-* не разворачивалась в RTL. Замена по таблице — один-в-один, суффиксы брейкпоинтов переносятся как есть: .gr-ml-4-md.gr-ms-4-md. Сторона, нужная буквально, — угол окна у кнопки «закрыть» — задаётся своим CSS: за геометрию экрана утилиты вдоль строки больше не отвечают. Верх и низ (t/b) остаются физическими: блочная ось в RTL не разворачивается.

Адаптивные варианты есть у всей логической шкалы: ms/me/ps/pe несут и оконные (.gr-ms-4-md), и контейнерные (.gr-ms-4-cmd) суффиксы — наравне с .gr-text-start-md. У start/end в позиционировании адаптивных вариантов нет, как не было их и у физической привязки.

Размеры (width, height, min-*, max-*) намеренно оставлены физическими: direction: rtl инлайн-ось не переставляет — это делает только writing-mode, а вертикального письма библиотека не поддерживает.

Spacing (scss/_spacing.scss)

Margin и padding по шкале с шагом 0.25rem. Ступеней двенадцать: 0 1 2 3 4 5 6 7 8 10 12 16 — мелкий шаг до 2rem, дальше секционные отступы 2.5rem, 3rem и 4rem.

КлассЗначение
.gr-m-{n}margin
.gr-p-{n}padding
.gr-mt-{n}, .gr-mb-{n}, .gr-pt-{n}, .gr-pb-{n}Верх и низ, физические
.gr-ms-{n}, .gr-me-{n}, .gr-ps-{n}, .gr-pe-{n}По сторонам строки, логические
.gr-mx-{n}, .gr-my-{n}, .gr-px-{n}, .gr-py-{n}По осям (margin-inline, padding-inline)
.gr-mx-auto, .gr-ms-auto, .gr-me-autoАвто-маржины

Адаптивные варианты — суффиксом: .gr-m-4-sm, .gr-p-8-md, .gr-mx-auto-lg, .gr-ms-4-md. Есть у всех свойств шкалы и у всех авто-маржинов, включая логические.

Произвольные значения / Arbitrary values

Разовое значение прямо в имени класса — то, чего статический файл выразить не может: список возможных значений неизвестен до того, как страница написана.

<script src="griffincss.js"></script>
<script src="griffincss-utils.js"></script>

<div class="gr-mt-[13px]">отступ сверху 13px</div>
<div class="gr-w-[42%]">ширина 42%</div>
<div class="gr-p-[calc(1rem+3px)]">паддинг из calc()</div>

Без JS не работает и работать не может. Класса .gr-mt-[13px] в собранном CSS нет — правило создаётся рантаймом в момент, когда он видит элемент, и уезжает в тот же слой griffincss.utils. Не выполнился скрипт — отступа нет. Поэтому произвольным значением задаётся оформление, а не то, от чего зависит читаемость страницы; шкала .gr-mt-4 работает всегда и остаётся основным способом.

Свойства, которые понимает рантайм: отступы (m, mt, mb, ms, me, mx, my и те же с p), gr-radius-, gr-gap-, размеры (w, h, min-w, max-w, min-h, max-h), смещения (top, bottom, логические start, end, inset) и gr-z-. Набор объявлений повторяет статический класс один в один: .gr-p-[13px], как и .gr-p-4, пишет и --gr-p, и padding, поэтому каскад скруглений видит зазор в обоих случаях.

У gr-text- произвольного варианта нет намеренно: .gr-text-2xl — кегль, .gr-text-white — цвет, и .gr-text-[13px] пришлось бы угадывать.

Содержимое скобок — ввод из разметки, и проверяется он так же, как строка раскладки в ядре: не длиннее 48 символов, без {, }, ; и /*. Невалидное содержимое и неизвестное свойство дают console.warn и пропуск класса, а не сломанное правило. Пробела в значении не бывает по устройству разметки — составное пишется слитно: gr-mt-[calc(1px+2px)].

Подробности и живые примеры — docs/arbitrary-values.html.

Sizing (scss/_sizing.scss)

ГруппаКлассы
Ширина.gr-w-full / screen / auto / fit / min / max, дроби .gr-w-1/2, .gr-w-1/3, .gr-w-2/3, .gr-w-1/4, .gr-w-3/4
Высота.gr-h-full / screen / auto / fit
Минимум.gr-min-w-0 / full, .gr-min-h-0 / full / screen
Максимум.gr-max-w-none / full / page, .gr-max-h-full / screen

Слэш в дробных классах пишется в разметке как есть — class="gr-w-1/2"; экранирование нужно только внутри CSS-селектора и делается при сборке. screen — это 100svw / 100svh: малые единицы вьюпорта не пляшут вместе с панелями мобильного браузера и не учитывают полосу прокрутки. .gr-max-w-pagevar(--gr-max-width), та же ширина, что у .gr-container.

Адаптивные варианты есть у width и height: .gr-w-1/2-md, .gr-h-full-lg. У min-* и max-* их нет — они задают рамку, одинаковую на всех ширинах.

Borders (scss/_borders.scss)

Ширина, стороны и стиль границы. Цвет — в Colors, скругления — в Border Radius.

ГруппаКлассы
Ширина.gr-border (1px), .gr-border-0, .gr-border-2, .gr-border-4, .gr-border-8
Стороны.gr-border-t / b, логические .gr-border-s / e
Оси.gr-border-x (border-inline), .gr-border-y (border-block)
Сторона заданной ширины.gr-border-{t,b,s,e,x,y}-{0,2,4,8} — например .gr-border-s-4
Стиль.gr-border-solid / dashed / dotted / double / none

Каждый класс ширины самодостаточен — задаёт и стиль, и var(--gr-color-border), поэтому .gr-border-2 виден сам по себе. Класс цвета из Colors подключается поверх: class="gr-border-2 gr-border-primary".

Ширину у отдельной стороны задаёт свой класс — .gr-border-s-4, а не пара .gr-border-s + .gr-border-4: класс ширины пишет сокращённое свойство border и сбрасывает вместе с ним все четыре стороны.

<blockquote class="gr-border-s-4 gr-border-danger gr-ps-4">Цитата</blockquote>

<!-- в тексте справа налево та же полоска сама перейдёт вправо -->
<blockquote dir="rtl" class="gr-border-s-4 gr-border-danger gr-ps-4">اقتباس</blockquote>

Shadows (scss/_shadows.scss)

КлассТокен
.gr-shadow-xs.gr-shadow-2xl--gr-shadow-xs--gr-shadow-2xl
.gr-shadow-inner--gr-shadow-inner — вдавленность
.gr-shadow-nonebox-shadow: none

Цвет отделён от геометрии: --gr-shadow-color в формате HSL-компонент перекрашивает все семь теней разом.

:root { --gr-shadow-color: 220, 40%, 20%; } /* холодная тень вместо чёрной */

Position (scss/_position.scss)

ГруппаКлассы
Схема.gr-static / relative / absolute / fixed / sticky
Края.gr-inset-0, .gr-inset-auto, .gr-inset-x-0 (inset-inline), .gr-inset-y-0 (inset-block), .gr-top-0 / .gr-bottom-0 и -auto, логические .gr-start-0 / .gr-end-0 и -auto
Края строки.gr-start-0, .gr-end-0, .gr-start-auto, .gr-end-auto — логические
Наложение.gr-z-0.gr-z-50 (шаг 10), .gr-z-auto

Адаптивные варианты только у схемы: .gr-sticky-md, .gr-static-lg.

Effects (scss/_effects.scss)

ГруппаКлассы
Прозрачность.gr-opacity-0 / 25 / 50 / 75 / 100
Вписывание медиа.gr-object-cover / contain / fill / none / scale-down
Точка кадра.gr-object-pos-center / top / right / bottom / left
Пропорции.gr-aspect-square / video / wide / portrait / auto

Filters (scss/_filters.scss)

ГруппаКлассы
Размытие.gr-blur-1 / 2 / 3 / 4 — 2, 4, 8 и 16 px
Обесцвечивание.gr-grayscale-50 / 100
Яркость.gr-brightness-75 / 90 / 110 / 125
Сброс.gr-filter-none — снимает фильтр целиком

Все функции живут в одном свойстве filter, поэтому класс задаёт настраиваемое свойство (--gr-blur, --gr-grayscale, --gr-brightness), а собирающее правило склеивает их в значение. Без этого .gr-blur-2 и .gr-grayscale-100 на одном элементе затирали бы друг друга. Нейтральные значения в том же правиле снимают наследование: вложенный .gr-grayscale-100 не перенимает размытие предка.

Контраст и насыщенность машинерия поддерживает, но по умолчанию не выводит. Спрос на них ниже, а трансформации, фильтры и градиенты и без них выбирают потолок веса утилит. Пустая карта стоит ноль байт; непустая включает и классы, и функцию в собирающем правиле:

@include meta.load-css('griffincss-utils/scss/filters', $with: (
  "gr-saturations": (0: 0, 150: 1.5),
  "gr-contrasts": (75: 0.75, 125: 1.25)
));

Получите .gr-saturate-0, .gr-saturate-150, .gr-contrast-75, .gr-contrast-125.

Размытие большого блока стоит кадров. filter: blur() заставляет браузер растеризовать элемент отдельным слоем и размывать его каждый кадр; на слабом устройстве размытая подложка во весь экран заметно роняет прокрутку. Размывайте маленькое и статичное, а не большое и движущееся.

Gradients (scss/_gradients.scss)

ГруппаКлассы
Направление.gr-gradient-to-r / l / t / b / br / tr
Начало.gr-from-white, .gr-from-black, .gr-from-primary, secondary, success, warning, danger, info
Середина.gr-via-* — те же восемь имён
Конец.gr-to-* — те же восемь имён

Класс направления обязателен: он и рисует градиент. Незаданные концы прозрачны, поэтому .gr-gradient-to-t .gr-from-black — это готовая подложка под подпись на фотографии. Средняя точка необязательна и не стоит ничего, пока не задана: --gr-via подставляется в тот же linear-gradient пустотой.

Своей палитры у группы нет: цвета берутся из тех же карт, что и .gr-bg-*, то есть из переменных --gr-hsl-*. Восемь имён вместо восемнадцати — плата за бюджет: три точки на всю палитру акцентов дали бы 54 класса. Нужны остальные оттенки — переопределите $gr-gradient-colors. Имя, которого в палитре нет, молча пропускается, поэтому перенастроенный под свой бренд $gr-accents не оставит классов, ссылающихся в пустоту.

Суффиксов брейкпоинта нет ни у фильтров, ни у градиентов — как и у трансформаций. .gr-blur-2-md и .gr-gradient-to-r-lg не существуют: декоративный фон не меняется от ширины окна, а девятикратная группа не помещается в бюджет утилит.

Interactivity (scss/_interactivity.scss)

ГруппаКлассы
Курсор.gr-cursor-pointer / default / text / move / grab / grabbing / wait / progress / help / not-allowed
События.gr-pointer-events-none / auto
Выделение.gr-user-select-none / all / text / auto
Прокрутка.gr-scroll-smooth / auto

.gr-scroll-smooth объявлен внутри @media (prefers-reduced-motion: no-preference).

Transitions (scss/_animations.scss)

ГруппаКлассы
Переходы.gr-transition, .gr-transition-colors / opacity / transform / shadow / none
Длительность.gr-duration-75.gr-duration-1000 (75, 100, 150, 200, 300, 500, 700, 1000 мс)
Анимация.gr-animate-spingr-spin 1s linear infinite

Библиотеки анимаций (fade, slide, pulse) в Griffincss нет намеренно: это задача уровня приложения. При prefers-reduced-motion: reduce переходы сжимаются до 0.01ms, вращение замедляется до 3 секунд.

Transforms (scss/_transforms.scss)

ГруппаКлассы
Масштаб.gr-scale-95 / 100 / 105 / 110
Поворот.gr-rotate-3 / 6 / 12 / 45 / 90 и та же пятёрка со знаком минус: .gr-rotate--3.gr-rotate--90
Сдвиг.gr-translate-x-1 / 2, .gr-translate-y-1 / 2 и те же со знаком: .gr-translate-y--1
Наведение.gr-hover-lift — подъём на 0,25 rem, .gr-hover-grow — увеличение до 1,03

Суффиксов брейкпоинта у группы нет и не будет. .gr-scale-105-md не существует — как не существует .gr-radius-4-md. Довод тот же: эффект наведения не меняется от ширины окна, а адаптивные и контейнерные варианты умножили бы группу примерно вдевятеро и съели бы весь бюджет утилит. Нужен масштаб от ширины — это свой @media в вашем CSS, одно правило вместо девяноста.

Трансформация собирается из раздельных свойств scale, rotate и translate, а не из общего transform: три класса на одном элементе складываются, не затирая друг друга. Обе оси сдвига живут в одном свойстве translate, поэтому значение приходит через --gr-tx и --gr-ty, а нулём по соседней оси снимается наследование.

:hover объявлен только внутри @media (hover: hover) — на сенсорном экране наведение залипает после касания. Переход у обоих классов свой, поэтому они работают в одиночку; при prefers-reduced-motion: reduce он сжимается до 0.01ms.

Учтите: .gr-transition-transform из модуля Transitions анимирует свойство transform и на раздельные scale / rotate / translate не действует. Для них берут .gr-transition (все свойства) или пишут своё правило.

Typography (scss/_typography.scss)

Шрифтовые утилиты: размер, насыщенность, выравнивание, межстрочный интервал, трансформация, декор, переносы, семейства. Все классы имеют адаптивные варианты с суффиксами -sm, -md, -lg, -xl — кроме относительных размеров, см. ниже.

ГруппаКлассы
Размер.gr-text-xs.gr-text-6xl (12px … 60px)
Относительный размер.gr-text-rel-xs.gr-text-rel-3xl (calc(1em - 4px)calc(1em + 8px))
Насыщенность.gr-font-thin (100) … .gr-font-black (900)
Выравнивание.gr-text-center / justify, логические .gr-text-start / end
Трансформация.gr-uppercase / lowercase / capitalize
Интервал.gr-leading-tight (1.25) … .gr-leading-loose (2)
Декор.gr-underline / no-underline / line-through
Стиль.gr-italic / not-italic, .gr-tabular-nums (цифры одной ширины)
Переносы.gr-break-words / truncate / whitespace-*
Перенос строк.gr-text-balance (заголовки), .gr-text-pretty (абзацы)
Обрезка по строкам.gr-line-clamp-1.gr-line-clamp-6, .gr-line-clamp-none
Списки.gr-list-none / disc / decimal / inside / outside
Семейства.gr-font-sans / serif / mono

Относительные размеры

.gr-text-* задают размер в rem — от корня документа, независимо от окружения. Это правильное поведение для страницы, но неудобное для блока, который показывают в разных местах шаблона: виджет в шапке и он же в подвале должны отличаться размером, а содержимое у них одно.

.gr-text-rel-* не задают размер, а сдвигают текущий на фиксированный шаг в пикселях:

КлассЗначениеПри родителе 16px
.gr-text-rel-xscalc(1em - 4px)12px — как .gr-text-xs
.gr-text-rel-smcalc(1em - 2px)14px — как .gr-text-sm
.gr-text-rel-lgcalc(1em + 2px)18px — как .gr-text-lg
.gr-text-rel-xlcalc(1em + 4px)20px — как .gr-text-xl
.gr-text-rel-2xlcalc(1em + 6px)22px
.gr-text-rel-3xlcalc(1em + 8px)24px

Шаг задан в пикселях, а не долей em, намеренно: от базовых 16px четыре младшие ступени попадают в базовую шкалу ровно, без округлений. Ступени base в наборе нет — класс, не меняющий размер, не нужен, его роль играет отсутствие класса.

<!-- виджет целиком мельче окружения, вложенные пропорции сохранены -->
<article class="widget gr-text-rel-sm">
  <h3 class="gr-text-rel-xl">Заголовок</h3>
  <p>Текст из БД — размер неизвестен на этапе вёрстки шаблона.</p>
  <footer class="gr-text-rel-xs">Сноска</footer>
</article>

Четыре особенности, о которых стоит помнить:

Шкала обрывается на 3xl: крупнее — это уже не подстройка блока под окружение, а отдельный заголовок, для которого есть базовый набор.

Visibility (scss/_visibility.scss)

КлассОписание
.gr-hiddendisplay: none
.gr-blockdisplay: block
.gr-inlinedisplay: inline
.gr-inline-blockdisplay: inline-block
.gr-invisible / .gr-visiblevisibility — место под элемент сохраняется
.gr-overflow-* / -x-* / -y-*auto, hidden, clip, scroll, visible
.gr-scroll-clipОбрезает полосу прокрутки по --gr-r: без неё она срезает скруглённые углы блока без рамки
.gr-sr-onlyСкрыт визуально, доступен скринридеру (clip-path: inset(50%))
.gr-sr-only-focusableТо же, но появляется при фокусе с клавиатуры — ссылка «к содержимому»
.gr-not-sr-onlyВернуть в поток
.gr-hidden-sm / md / lg / xlСкрытие от брейкпоинта

Полосу прокрутки движок рисует поверх собственного края элемента и по border-radius не обрезает: у скруглённого блока с overflow: auto она срезает углы, а если у блока есть рамка — съедает и её нижнюю сторону. Приёма два, и выбор между ними не вкусовой:

<!-- фон и скругление, рамки нет: хватает одного класса -->
<div class="gr-radius-4 gr-overflow-x-auto gr-scroll-clip">…</div>

<!-- есть рамка: рамка и радиус снаружи, прокрутка внутри -->
<div class="gr-radius-4 gr-overflow-hidden gr-border">
  <p class="gr-overflow-x-auto gr-whitespace-nowrap">…</p>
</div>

.gr-scroll-clip режет полосу через clip-path, беря радиус из --gr-r — переменной, которую публикуют утилиты со значением (.gr-radius-4, .gr-radius-full и прочие); у голого .gr-radius своего значения нет, он читает переменную, а не задаёт её.

Рамку обрезка не спасает: полоса рисуется поверх нижней границы, и вернуть эту границу свойствами самого блока нельзя — нужен внешний узел. Он же обрезает полосу по радиусу обычным overflow: hidden. clip-path вдобавок режет внешнюю тень и контур фокуса, так что блоку с тенью нужна та же вложенность.

.gr-overflow-{auto,hidden,clip,scroll,visible}Обе оси
.gr-overflow-x-* / .gr-overflow-y-*Каждая ось отдельно

Border Radius Cascade (scss/_border-radius.scss)

Автоматический каскадный border-radius. Прямые потомки .gr-radius получают радиус max(0, --gr-r − --gr-p). Зазор задаёте вы переменной --gr-p, а padding контейнера выводится из неё — поэтому первый уровень работает на чистом CSS, без JS. Произвольную глубину достраивает опциональный рантайм (ниже).

КлассОписание
.gr-radiusКонтейнер каскадного радиуса: padding: var(--gr-p)
.gr-radius-0.gr-radius-16Предопределённый радиус (шаг 0.25rem, 0 … 4rem)
.gr-radius-fullПолное скругление (9999px)
--gr-rРадиус контейнера (задаётся классами или вручную)
--gr-pЗазор между контейнером и детьми; из него выводится padding
--gr-r-down, --gr-r-gapПубликация вниз: их читают дети, а не --gr-r/--gr-p

Обе переменные зарегистрированы через @property как ненаследуемые, поэтому вложенный .gr-radius без собственного --gr-r даёт квадрат, а не радиус предка. Это осознанный отказ: наследуйся --gr-r, вложенный контейнер тихо копировал бы радиус родителя один в один.

Зазор — одно значение на все стороны. .gr-px-*, .gr-py-* и односторонние отступы --gr-p не объявляют и с каскадом не работают; равномерный .gr-p-* — работает, он задаёт --gr-p и выводит padding из него. Процентный радиус (--gr-r: 50%) каскаду тоже не годится: проценты считаются от коробки самого элемента, и концентричности углов не выходит.

Адаптивных классов радиуса нет: значение приходит из --gr-r, поэтому меняется обычным медиа-запросом, а каскад по вложенности пересчитывается сам.

.card { --gr-r: 0; }
@media (width >= 768px) { .card { --gr-r: 1rem; } }

Пример / Example:

<!-- --gr-p даёт и зазор, и padding контейнера -->
<div class="gr-radius" style="--gr-r: 30px; --gr-p: 5px;">
  <div>border-radius: 25px автоматически</div>
</div>

<!-- Зазор классом — то же самое: .gr-p-4 объявляет --gr-p: 1rem -->
<div class="gr-radius gr-radius-8 gr-p-4">
  <div>border-radius: 32 − 16 = 16px</div>
</div>

Нужен padding, не равный зазору, — задайте его отдельно: собственное свойство перекроет вывод из переменной.

Рантайм каскада (dist/griffincss-utils.js)

Опциональный второй рантайм, peer к ядру. Даёт то, чего статический CSS выразить не может, — рекурренту r(n) = max(0, r(n−1) − p(n−1)) на произвольную глубину. Явный радиус тогда нужен только верхнему контейнеру.

<script src="griffincss.js"></script>
<script src="griffincss-utils.js"></script>

<div class="gr-radius gr-radius-16 gr-p-3">   <!-- 64px, зазор 12 -->
  <div class="gr-radius" style="--gr-p: 10px;"> <!-- 64−12 = 52px -->
    <div class="gr-radius gr-p-2">              <!-- 52−10 = 42px -->

Замеров он не делает. Ни getComputedStyle, ни forced reflow: рантайм складывает строку из объявленных значений и отдаёт арифметику CSS — вложенный контейнер получает одно объявление --gr-r: max(0px, calc(4rem - 0.75rem)). Смешанные единицы (rem из класса, px из style) разрешает calc(). Правило выдаётся в слой griffincss.utils один раз на форму вложенности: одинаково вложенные блоки делят класс.

Зазор читается и из класса .gr-p-*, и из style — источники равноправны, при расхождении побеждает инлайн и печатается предупреждение.

ЧтоКогда
Потоковый режимВо время разбора документа: контейнер получает радиус в момент появления, а не на DOMContentLoaded
Наблюдение за деревомПосле загрузки: вставленное поддерево обрабатывается само, с дебаунсом 16 мс
Перенос поддереваПрежний выведенный класс снимается, радиус пересчитывается от нового предка
Атрибут на <script>Действие
data-auto="false"Не запускаться вовсе
data-stream="false"Без потокового режима: один проход на DOMContentLoaded
data-observe="false"Без наблюдения за живым деревом

Ручное управление — window.Griffincss.utils:

Griffincss.utils.radiusCascade(node);  // пересчитать поддерево
Griffincss.utils.observe(node);        // наблюдать за поддеревом
Griffincss.utils.stop();               // снять всё наблюдение

Без скрипта работают первый уровень и его прямые дети: вложенные контейнеры без явного --gr-r остаются квадратными.

Своя палитра и своя шкала

Всё, что задаёт состав вывода утилит, помечено !default: собирающий библиотеку из исходников меняет палитру и шкалы, не правя файлы пакета. Вычисляемые значения не помечены намеренно — переопределять производную от уже переопределяемого значит завести второй источник правды.

ФайлПеременнаяЧто задаёт
_palette.scss$gr-graysНейтральная шкала: имена .gr-bg-gray-* и переменная-источник
_palette.scss$gr-accentsАкценты: имена .gr-bg-primary и соседей
_typography.scss$gr-text-sizesШкала кегля .gr-text-xs.gr-text-6xl вместе с адаптивными и контейнерными вариантами
_typography.scss$gr-text-rel-stepsОтносительные ступени .gr-text-rel-*
_typography.scss$gr-weightsНасыщенность .gr-font-*
_typography.scss$gr-leadingМежстрочный интервал .gr-leading-*
_spacing.scss$gr-spacing-baseШаг шкалы отступов
_spacing.scss$gr-spacing-stepsСтупени .gr-m-*, .gr-p-* и всей их родни
_sizing.scss$gr-widthsШирины .gr-w-*, включая дробные
_sizing.scss$gr-heightsВысоты .gr-h-*
_borders.scss$gr-border-sidesСтороны .gr-border-t.gr-border-e
_borders.scss$gr-border-axesОси .gr-border-x, .gr-border-y
_border-radius.scss$gr-radius-baseШаг шкалы скруглений
_border-radius.scss$gr-radius-maxВерхняя ступень .gr-radius-*
_effects.scss$gr-opacitiesСтупени .gr-opacity-*
_position.scss$gr-positionsСхемы .gr-relative, .gr-sticky и соседи
_shadows.scss$gr-shadowsСтупени .gr-shadow-*
_filters.scss$gr-blursСтупени .gr-blur-*
_filters.scss$gr-grayscalesСтупени .gr-grayscale-*
_filters.scss$gr-brightnessesСтупени .gr-brightness-*
_filters.scss$gr-contrastsСтупени .gr-contrast-*; по умолчанию карта пуста
_filters.scss$gr-saturationsСтупени .gr-saturate-*; по умолчанию карта пуста
_gradients.scss$gr-gradient-directionsНаправления .gr-gradient-to-*
_gradients.scss$gr-gradient-colorsЦвета точек .gr-from-*, .gr-via-*, .gr-to-*
_transforms.scss$gr-scalesСтупени .gr-scale-*
_transforms.scss$gr-rotationsСтупени поворота: каждая даёт пару классов со знаком
_transforms.scss$gr-translatesСтупени сдвига .gr-translate-*
_interactivity.scss$gr-cursorsКурсоры .gr-cursor-*

Список закрыт инвариантом: переменная верхнего уровня в публичном модуле утилит обязана нести !default либо запись в явном списке исключений с причиной. Новая карта без пометки роняет npm test.

Своя точка входа вместо готовой

Модули утилит выводят CSS, поэтому @use … with рядом с готовой точкой входа делает не то, чего от него ждут: модуль попадает в вывод дважды, и первый экземпляр оказывается вне слоя griffincss.utils — то есть выигрывает у всей остальной библиотеки. Замер такой сборки — 14 952 Б gzip против 14 931 Б у правильной с той же палитрой: вторая копия почти целиком сжимается в обратную ссылку, и по весу ловушку не заметить вовсе. Ломает она не вес, а каскад: незаслоённая копия выигрывает и у остальной библиотеки, и у вашего собственного CSS в слоях.

// НЕ ТАК: два экземпляра модуля, первый вне слоёв
@use 'griffincss-utils/scss/colors' with ($gr-accents: (…));
@use 'griffincss-utils/scss/griffincss-utils';

Рабочая форма — своя точка входа: тот же список модулей, что в packages/utils/scss/griffincss-utils.scss, но у тех, чей состав меняется, стоит $with. Имена переменных внутри $with пишутся строками, без $.

Обе половины: карта и переменные под неё

Карта задаёт имена классов и то, из какой переменной берётся цвет; сами --gr-hsl-* приходят из темы ядра. Переопределив карту и не заведя переменных, вы получите классы, ссылающиеся в пустоту. Поэтому пример из одной половины — ловушка, и ниже их две.

// my-griffincss-utils.scss — своя точка входа вместо готовой
@use 'sass:meta';

@layer griffincss.reset, griffincss.tokens, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style, griffincss.hidden;

// Токены — в общий слой tokens, как у всех сборок: копия та же, что
// в ядре, и подслой темы после tokens её перебивает. Только токены темы:
// правила режима со свойствами (theme-rules) в утилиты не подключают —
// из старшего слоя они побеждали бы компоненты.
@layer griffincss.tokens {
  @include meta.load-css('griffincss-core/scss/tokens');
  @include meta.load-css('griffincss-core/scss/theme-tokens');
}

@layer griffincss.utils {
  @include meta.load-css('griffincss-utils/scss/keyframes');
  @include meta.load-css('griffincss-utils/scss/animations');
  @include meta.load-css('griffincss-utils/scss/border-radius');
  @include meta.load-css('griffincss-utils/scss/borders');

  // ПЕРВАЯ ПОЛОВИНА: свои имена классов и свой источник цвета.
  @include meta.load-css('griffincss-utils/scss/colors', $with: (
    "gr-accents": (
      brand:       var(--gr-hsl-brand),
      brand-dark:  var(--gr-hsl-brand-dark),
      danger:      var(--gr-hsl-danger)
    )
  ));

  @include meta.load-css('griffincss-utils/scss/effects');
  @include meta.load-css('griffincss-utils/scss/filters');
  @include meta.load-css('griffincss-utils/scss/gradients');
  @include meta.load-css('griffincss-utils/scss/interactivity');
  @include meta.load-css('griffincss-utils/scss/position');
  @include meta.load-css('griffincss-utils/scss/shadows');
  @include meta.load-css('griffincss-utils/scss/sizing');
  @include meta.load-css('griffincss-utils/scss/spacing');
  @include meta.load-css('griffincss-utils/scss/transforms');
  @include meta.load-css('griffincss-utils/scss/typography');
  @include meta.load-css('griffincss-utils/scss/visibility');
}

// [hidden] старше любого display — последним слоем, как у всех сборок.
@layer griffincss.hidden {
  @include meta.load-css('griffincss-core/scss/hidden');
}

// ВТОРАЯ ПОЛОВИНА: переменные, на которые ссылается карта.
// Без неё .gr-bg-brand в CSS выйдет, но покажет пустоту:
// hsl(var(--gr-hsl-brand)) не из чего вычислить.
// --gr-hsl-danger объявлять не нужно — он приходит из темы ядра.
:root {
  --gr-hsl-brand:      262, 83%, 58%;
  --gr-hsl-brand-dark: 262, 83%, 45%;
}
sass my-griffincss-utils.scss my-griffincss-utils.css \
  --load-path=node_modules --style=compressed --no-source-map

Результат: .gr-bg-brand, .gr-text-brand-dark, .gr-border-danger и ни одного .gr-bg-primary. Вес — 14 974 Б gzip против 15 394 Б у обычной сборки: восемнадцать акцентов сменились тремя, серая шкала осталась на месте. Градиенты подстроились сами — .gr-from-primary исчез вместе с акцентом, остались .gr-from-white, .gr-from-black и .gr-from-danger.

Готовую точку входа при этом не подключают. Модуль, загруженный с $with, не может быть загружен раньше — а griffincss-utils.scss загрузит его без настройки. Свой файл заменяет готовый, а не дополняет его. Для -scoped-сборки копируется griffincss-utils-scoped.scss: там те же модули, но внутри :where(.griffin).

Сократить шкалу

Второй сценарий и самый частый у того, кто борется за вес: состав не заменяют, а урезают. Три строки — половина ступеней отступа, восемь ступеней скругления из шестнадцати, четыре кегля из десяти:

@include meta.load-css('griffincss-utils/scss/border-radius', $with: (
  "gr-radius-max": 8
));
@include meta.load-css('griffincss-utils/scss/spacing', $with: (
  "gr-spacing-steps": (0, 1, 2, 4, 8)
));
@include meta.load-css('griffincss-utils/scss/typography', $with: (
  "gr-text-sizes": (sm: 0.875rem, base: 1rem, lg: 1.25rem, 2xl: 1.5rem)
));

Замер: 14 945 → 10 876 Б gzip (−27,2 %), классов 2490 → 1546. Шкала кегля тянет за собой адаптивные и контейнерные варианты, поэтому платит за себя больше остальных; после размена односторонних отступов на логические то же делает и шкала отступов — логические классы несут полные адаптивные наборы.

Целая группа убирается ещё проще — строкой @include меньше: не нужны тени, не подключайте shadows. Отсечение неиспользуемого (npm run purge) решает ту же задачу по факту разметки; переопределение решает её по решению, до сборки.

Рантайм тут ни при чём. Переопределение на компилтайме меняет состав и имена классов в CSS. Рантайм утилит (griffincss-utils.js) разбирает произвольные значения gr-*-[…] и каскад скруглений по своей таблице, которая читается из исходников пакета при сборке. Это два несвязанных механизма: урезав шкалу $gr-spacing-steps, вы не отнимете у рантайма gr-p-[13px], а переименовав акценты — не научите его новым именам.