Справочник утилит
Отступы, размеры, типографика, цвета, границы, скругления, тени, позиционирование, эффекты, видимость, интерактивность и переходы.
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-theme | light, dark, auto | следует prefers-color-scheme |
data-gr-a11y | low-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-auto | margin-inline-start / -end: auto |
.gr-text-left / .gr-text-right | .gr-text-start / .gr-text-end | text-align: start / end |
.gr-left-0 / .gr-right-0 | .gr-start-0 / .gr-end-0 | inset-inline-start / -end |
.gr-border-l / .gr-border-r | .gr-border-s / .gr-border-e | border-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-page — var(--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-none | box-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-spin — gr-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-xs | calc(1em - 4px) | 12px — как .gr-text-xs |
.gr-text-rel-sm | calc(1em - 2px) | 14px — как .gr-text-sm |
.gr-text-rel-lg | calc(1em + 2px) | 18px — как .gr-text-lg |
.gr-text-rel-xl | calc(1em + 4px) | 20px — как .gr-text-xl |
.gr-text-rel-2xl | calc(1em + 6px) | 22px |
.gr-text-rel-3xl | calc(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>
Четыре особенности, о которых стоит помнить:
- Шаг фиксирован, а не пропорционален. При родителе 32px
.gr-text-rel-smдаст 30px, а не 28px. Это плата за точное попадание в шкалу от 16px. - Шаги складываются при вложении.
.gr-text-rel-smвнутри.gr-text-rel-smдаётcalc(1em - 2px)от уже уменьшенного размера, то есть −4px суммарно. Это и есть смысл набора — блок сдвигается целиком, — но каскадировать его глубже двух уровней обычно не нужно. - Базовый
.gr-text-*внутри обрывает цепочку. Он вremи вернёт абсолютный размер независимо от родителя. Смешивать оба набора в одном поддереве — верный способ получить неожиданный результат. - Адаптивных и контейнерных вариантов у набора нет намеренно. Размер здесь задаёт вложенность, а не ширина окна: чтобы виджет реагировал ещё и на своё окружение, поставьте
.gr-text-rel-*внутри, а размер корня меняйте контейнерным.gr-text-sm-cmd.
Шкала обрывается на 3xl: крупнее — это уже не подстройка блока под окружение, а отдельный заголовок, для которого есть базовый набор.
Visibility (scss/_visibility.scss)
| Класс | Описание |
|---|---|
.gr-hidden | display: none |
.gr-block | display: block |
.gr-inline | display: inline |
.gr-inline-block | display: inline-block |
.gr-invisible / .gr-visible | visibility — место под элемент сохраняется |
.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], а переименовав акценты — не научите его новым именам.