Border Radius
Каскад скруглений: дочерние блоки наследуют уменьшенный радиус автоматически. Готовые классы от 0 до 4rem + pill. Зазор --gr-p задаётся переменной, поэтому первый уровень работает без JS. Произвольную глубину достраивает опциональный griffincss-utils.js — без единого замера в рантайме.
Концепция
padding контейнера = --gr-p
child_radius = max(0, --gr-r − --gr-p)
Зазор один на все стороны, и обе переменные ненаследуемые: значение уровня объявляется на нём самом,
а вниз уходит отдельная публикация — --gr-r-down и --gr-r-gap.
| Переменная | Назначение | По умолчанию |
|---|---|---|
--gr-r |
border-radius контейнера | 0px |
--gr-p |
Зазор между контейнером и детьми; из него выводится padding |
0px |
Зазор — источник, padding — следствие: .gr-radius выводит свой padding из --gr-p, поэтому значение известно CSS и замеры в рантайме не нужны. Нужен padding, не равный зазору, — задайте его отдельно: собственное свойство перекроет вывод из переменной.
Готовые классы
Вместо ручного style="--gr-r: 16px" используйте классы — чище, короче, читаемее.
Шаг 0.25rem, от .gr-radius-0 до .gr-radius-16 (0–4rem).
| Класс | Значение | При px = 16 |
|---|---|---|
.gr-radius-0 | 0 | 0px |
.gr-radius-1 | 0.25rem | 4px |
.gr-radius-2 | 0.5rem | 8px |
.gr-radius-4 | 1rem | 16px |
.gr-radius-6 | 1.5rem | 24px |
.gr-radius-8 | 2rem | 32px |
.gr-radius-12 | 3rem | 48px |
.gr-radius-16 | 4rem | 64px |
Каждый класс устанавливает и --gr-r, и border-radius — каскад на детей работает автоматически.
<!-- Было (ручные переменные) -->
<div class="gr-radius" style="--gr-r: 32px; --gr-p: 16px;">
<!-- Стало (готовый класс) -->
<div class="gr-radius gr-radius-8" style="--gr-p: 16px;">
Базовое использование
.gr-radius-8 = 32px (2rem). --gr-p: 16px даёт и зазор, и padding: 16px.
Ребёнок: 32 − 16 = 16px.
<div class="gr-radius gr-radius-8" style="--gr-p: 16px;">
<div>Мой радиус = 32 − 16 = 16px</div>
</div>
Радиус: 32−16 = 16px. Зазор из --gr-p.
--gr-r задан классом .gr-radius-8, зазор и padding — переменной --gr-p.
Многоуровневый каскад
Каждый уровень — новый .gr-radius. Внутренние дети без .gr-radius получают радиус по формуле.
Вложенному контейнеру радиус нужен свой. --gr-r зарегистрирована через @property как ненаследуемая, поэтому вложенный .gr-radius без собственного значения даёт квадрат, а не радиус предка. Это осознанное поведение: наследуйся переменная, вложенный контейнер тихо копировал бы радиус родителя один в один — картинка правдоподобная и неверная. Многоуровневый каскад без ручных значений достраивает рантайм griffincss-utils.js.
Осевые и односторонние отступы каскаду не годятся. .gr-px-*, .gr-py-*, .gr-pt-* и прочие односторонние --gr-p не объявляют: у асимметричного отступа нет одного значения, от которого считать радиус ребёнка. Для каскада используйте равномерный .gr-p-* или --gr-p напрямую. По той же причине --gr-p принимает одну длину: --gr-p: 8px 20px недействительно — нужен неравномерный padding, задайте его свойством.
<div class="gr-radius gr-radius-12" style="--gr-p: 16px;">
<div class="gr-radius gr-radius-8" style="--gr-p: 8px;">
<div class="gr-radius gr-radius-6" style="--gr-p: 6px;">
<div>радиус = 24 − 6 = 18px</div>
</div>
</div>
</div>
Радиус: 24−6 = 18px (авто)
Уровень 1 (фиол.): 48px, зазор 16 → 2 (сирен.): 48−16 = 32px, зазор 8 → 3 (светл.): 32−8 = 24px, зазор 6 → внутр. блок: 24−6 = 18px.
Каждая ступень выбрана по формуле: класс уровня совпадает с тем, что даёт max(0, --gr-r − --gr-p) предыдущего.
Произвольная глубина: рантайм
Ручные значения на каждом уровне — обязанность, пока работает один статический CSS: рекуррента
r(n) = max(0, r(n−1) − p(n−1)) в нём невыразима. Подключите
griffincss-utils.js — и явный радиус нужен только верхнему контейнеру,
остальные рантайм выведет сам.
Зазор он читает и из класса, и из style — источники равноправны.
Классы .gr-p-* берутся из той же таблицы значений, из которой собран CSS, поэтому
.gr-p-3 и --gr-p: 12px для каскада — одно и то же. Шкала отступов идёт
ступенями по 0.25rem, то есть кратно 4px; всё, что между ступенями, задаётся
переменной. В примере ниже нечётные уровни — классы, чётные — произвольные значения.
<script src="griffincss.js"></script>
<script src="griffincss-utils.js"></script>
<div class="gr-radius gr-radius-16 gr-p-3"> <!-- 64px, зазор классом: 0.75rem -->
<div class="gr-radius" style="--gr-p: 10px;"> <!-- 64−12 = 52px, зазор свой: 10px -->
<div class="gr-radius gr-p-2"> <!-- 52−10 = 42px, классом: 0.5rem -->
<div class="gr-radius" style="--gr-p: 6px;"> <!-- 42−8 = 34px, своё: 6px -->
<div class="gr-radius gr-p-1"> <!-- 34−6 = 28px, классом: 0.25rem -->
<div>радиус = 28 − 4 = 24px</div>
</div>
</div>
</div>
</div>
</div>
Радиус: 28−4 = 24px (авто)
64px → 52 → 42 → 34 → 28 → 24. Явный радиус — только у верхнего контейнера, остальные вывел рантайм.
Зазоры: .gr-p-3 → --gr-p: 10px → .gr-p-2 → --gr-p: 6px → .gr-p-1.
Рантайм не считает — он складывает выражение. Второму уровню примера достаётся
--gr-r: max(0px, calc(4rem - 0.75rem)), третьему —
max(0px, calc(max(0px, calc(4rem - 0.75rem)) - 10px)): rem из класса и
px из style едут в одном выражении, и разрешает их CSS в calc().
Ни getComputedStyle, ни forced reflow — раскладку рантайм не трогает вовсе.
Одинаковая форма вложенности делит один класс и одно правило.
Процентный радиус каскаду не годится. --gr-r: 50% формально проходит:
и статическая формула, и рантайм соберут max(0px, calc(50% − 8px)), и браузер это применит.
Но проценты в border-radius считаются от коробки самого элемента, а не предка,
поэтому у ребёнка 50% — это половина уже другой коробки, и концентричности углов не получается.
Для каскада задавайте радиус длиной: классом .gr-radius-* или px/rem.
Поток и живое дерево. Пока страница разбирается, контейнер получает радиус
в момент появления, а не разом на DOMContentLoaded — как это делает парсер раскладок.
После загрузки рантайм наблюдает за <body>: вставленное поддерево обрабатывается само,
с дебаунсом 16 мс, а перенос поддерева к другому предку снимает прежний выведенный класс
и пересчитывает радиус. Ручное управление — Griffincss.utils.radiusCascade(node),
Griffincss.utils.observe(node), Griffincss.utils.stop().
Выключатели на теге <script>: data-stream="false" — без потокового режима,
data-observe="false" — без наблюдения за живым деревом,
data-auto="false" — не запускаться вовсе. Поток видно на
странице потоковой раскладки.
Класс и style на одном контейнере — только согласованные. Если задать оба
и с разными значениями, победит инлайн, а в консоли появится предупреждение
Griffincss: --gr-p задан и классом (1rem), и в style (16px) — значения расходятся.
Осевые и односторонние классы (.gr-px-*, .gr-py-*, .gr-pt-*)
зазором не считаются вовсе — рантайм их не видит.
Без скрипта работают первый уровень и его прямые дети. С data-auto="false" на теге
<script> (или без самого скрипта) вложенные контейнеры без явного --gr-r становятся квадратными —
внешний контейнер и его непосредственные дети остаются скруглёнными в любом случае.
Pill / полностью скруглённый
.gr-radius-full задаёт border-radius: 9999px — бесконечное скругление.
Используется для кнопок, бейджей, аватаров и любых pill-элементов.
<span class="gr-radius gr-radius-full" style="padding: 8px 20px;">Бейдж</span>
<button class="gr-radius gr-radius-full" style="padding: 12px 32px;">Кнопка</button>
Адаптивный радиус
Изменение в 0.7.0. Классов вида
.gr-radius-4-md больше нет. Полная сетка суффиксов стоила
68 классов при одном реальном сценарии на проект, а сам сценарий
решается одной переменной.
Радиус берётся из --gr-r, поэтому меняется он обычным
медиа-запросом — каскад по вложенности при этом продолжает работать:
дочерние блоки пересчитают свой радиус сами.
/* острые углы на мобильном, 16px от 768px */
.card { --gr-r: 0; }
@media (width >= 768px) {
.card { --gr-r: 1rem; }
}
<div class="gr-radius card" style="--gr-p: 16px;">
Адаптивный радиус
</div>
Несколько детей в контейнере
Каскад действует на всех прямых потомков .gr-radius (кроме тех, что сами .gr-radius).
<div class="gr-radius gr-radius-8" style="--gr-p: 16px;">
<header>Шапка</header>
<div>Контент</div>
<footer>Подвал</footer>
</div>
Три ребёнка. Радиус каждого: 32−16 = 16px. Одна настройка на всех.
Обнуление радиуса
Когда --gr-p ≥ --gr-r, дочерний радиус обнуляется: max(0, r − p).
<div class="gr-radius gr-radius-4" style="--gr-p: 8px;">
<div>16−8 = 8px (скруглён)</div>
</div>
<div class="gr-radius gr-radius-4" style="--gr-p: 16px;">
<div>max(0, 16−16) = 0px (острые углы)</div>
</div>
<div class="gr-radius gr-radius-4" style="--gr-p: 24px;">
<div>max(0, 16−24) = 0px (острые углы)</div>
</div>
p=8 < r=16
скруглён
p=16 = r=16
острые
p=24 > r=16
острые
Собственный радиус у ребёнка
border-radius напрямую на дочернем элементе перекрывает автоматический каскад.
<div class="gr-radius gr-radius-8" style="--gr-p: 12px;">
<div>Авто-радиус 20px</div>
<div style="border-radius: 6px;">Свой радиус 6px</div>
</div>
Как работает
SCSS-модуль генерирует каскадную базу и полный набор утилитарных классов:
// Регистрация: обе переменные ненаследуемые
@property --gr-r { syntax: "<length-percentage>"; inherits: false; initial-value: 0px; }
@property --gr-p { syntax: "<length-percentage>"; inherits: false; initial-value: 0px; }
// Каскад
.gr-radius {
padding: var(--gr-p);
border-radius: var(--gr-r);
// публикация вниз: дети читают её, а не --gr-r/--gr-p
--gr-r-down: var(--gr-r);
--gr-r-gap: var(--gr-p);
}
.gr-radius > :not(.gr-radius) {
border-radius: max(0px, calc(var(--gr-r-down, 0px) - var(--gr-r-gap, 0px)));
}
// Готовые классы (0.25rem шаг, 0–4rem)
.gr-radius-0 { --gr-r: 0; border-radius: 0; }
.gr-radius-1 { --gr-r: 0.25rem; border-radius: 0.25rem; }
...
.gr-radius-16 { --gr-r: 4rem; border-radius: 4rem; }
// Pill
.gr-radius-full { --gr-r: 9999px; border-radius: 9999px; }
// Адаптивные: .gr-radius-N-{sm,md,lg,xl}
JS не участвует. До v0.7.0 рантайм обходил все .gr-radius, читал min(padding-top, padding-left) из computed-стилей и выставлял --gr-p инлайном — это давало forced reflow и устаревало при изменении ширины. Теперь модель перевёрнута: --gr-p задаёте вы, padding выводится из него.
Советы
Используйте готовые классы. .gr-radius-8 вместо style="--gr-r: 32px" — чище, консистентно с остальной библиотекой и работает с адаптивными вариантами.
Не злоупотребляйте глубиной. После 2–3 уровней вложенности разница в радиусе незаметна. Для глубокой вложенности — border-radius: 0 на внутренних элементах.
Примеры использования
Скругление задаёт характер интерфейса, а каскад избавляет от арифметики вложенных радиусов. Рецепты ниже — три самых частых применения: карточка с медиа, капсульные элементы и склеенные группы, где радиус живёт только на внешних углах.
Карточка с каскадом
Классика: карточка с изображением, текстом и кнопкой. Один раз задаётся
радиус и внутреннее поле (.gr-radius-5 +
--gr-p: 16px) — картинка и кнопка получают согласованный
внутренний радиус сами, без вычислений «20 минус 16».
<div class="gr-radius gr-radius-5" style="--gr-p: 16px;">
<img src="photo.jpg" alt="">
<h3>Заголовок</h3>
<p>Описание карточки.</p>
<button>Подробнее</button>
</div>
Заголовок карточки
Все дети получают радиус 20−16 = 4px автоматически.
Сегмент-контрол: капсула в капсуле
Переключатель периодов или вкладок-фильтров — две вложенные капсулы:
серая подложка .gr-radius-full .gr-bg-sunken .gr-p-1
и активный сегмент — тоже .gr-radius-full, но на поверхности
и с тенью .gr-shadow-sm. Ряд сегментов держит флекс
с .gr-gap-sm; капсуле-подложке не нужен каскад — у полного
скругления внутренний радиус всё равно «до упора».
<div class="gr-flex gr-w-fit gr-gap-sm gr-radius-full gr-bg-sunken gr-p-1">
<button class="gr-btn gr-btn-sm gr-radius-full gr-bg-surface gr-shadow-sm">Месяц</button>
<button class="gr-btn gr-btn-sm gr-btn-ghost gr-radius-full">Год</button>
</div>
Склеенные группы: радиус только снаружи
Поле с кнопкой «Найти» или ряд слитых кнопок — задача «скругление
на внешних углах, между элементами — общий бордер». Руками это возня
с :first-child/:last-child; в ui она уже
проделана — .gr-input-group
и .gr-btn-group склеивают
содержимое сами. Радиус группы при этом остаётся на токене, так что
капсульный вариант — просто .gr-radius-full сверху.
<div class="gr-input-group">
<input class="gr-input" type="search" placeholder="Поиск" aria-label="Поиск">
<button class="gr-btn gr-btn-primary">Найти</button>
</div>