За базовый шаг системы «Ритм» взят межстрочный просвет — визуальное расстояние между строками абзаца.
TLDR
* {
text-box: trim-both ex alphabetic;
}
:root {
/* Высота строки минус высота строчной буквы */
--gap: calc(1rlh - 1rex);
--gap--relative: calc(1lh - 1ex);
}
p {
margin-block-start: calc(var(--gap) * 2);
}
small + h1 {
margin-block-start: var(--gap--relative);
}
Межстрочный просвет высчитывается как расстояние между базовыми линиями минус высота строчной буквы. Это значение — часть шрифтовых метрик, поэтому «Ритм» автоматически адаптируется к шрифту и применим к большинству проектов без дополнительной настройки. Единственная задача дизайнера — подобрать интерлиньяж.
Преимущества:
Простая ментальная модель. Минимально допустимое расстояние от элемента — gap, расстояние между его строками.
Адаптивность. Отступы масштабируются вместе с кеглем и высотой строки. Это упрощает работу с флюидной типографикой.
Согласованность со шрифтом. При смене шрифта отступы пересчитываются согласно его метрикам.
Как внедрить в проект
1. Ограничьте высоту контейнера с текстом до высоты строчной буквы. Это нужно, чтобы правильно считать просвет от границ блочных HTML-элементов:
* {
text-box: trim-both ex alphabetic;
}
2. Определите базовый отступ, равный межстрочному просвету:
:root {
--gap: calc(1rlh - 1rex);
}
3. Определите тот же отступ в относительных единицах для использования с увеличенными кеглями:
:root {
--gap--relative: calc(1lh - 1ex);
}
4. Задайте шкалу согласно потребностям проекта:
calc(var(--gap) * 0.5);
calc(var(--gap) * 0.75);
calc(var(--gap) * 1.5);
calc(var(--gap) * 2);
calc(var(--gap) * 3);
calc(var(--gap) * 4);
Мотивация
Модуль — базовая единица измерения, которой кратны размеры дизайн-системы. Модуль упрощает выбор размеров, делает решения объяснимыми и ускоряет перенос дизайна в код. Обычно он равен 8 пикселям.
В большинстве дизайн-систем модуль общий для всех измерений — для кегля, ширины и высоты, скруглений и отступов.
Но отступы на самом деле не подчиняются произвольному значению в 8px или 0.25rem. Они подчиняются интерлиньяжу — расстоянию между базовыми линиями строк текста.
Почему так
В ходе вёрстки мы постоянно пользуемся правилом, сформулированным в бюро Горбунова: «внутреннее ≤ внешнее». Внутренние расстояния типографического объекта должны быть не больше внешних.
Например, отступ от бирки до заголовка должен быть не меньше просвета между строками заголовка; отступ от иллюстрации до абзаца должен быть не меньше просвета между строками абзаца. Так, при выборе отступа дизайнер чаще всего смотрит на интерлиньяж.
Вот почему модуль отступов в системе «Ритм» равен межстрочному просвету:
:root {
/* В метриках шрифта просвет выражен как */
/* интерлиньяж минус высота строчной буквы: */
/* ascent + descent + lineGap − xHeight */
--gap: calc(1rlh - 1rex);
}
Браузерная поддержка
Использование подхода стало возможным в 2025 году благодаря поддержке свойств text-box, rlh, rex и lh. Браузерная поддержка — ≥ 81%.
Чтобы повысить поддержку, сгенерируйте полифилл для text-box с помощью инструмента «Икс-сайз».
Работа с PostCSS
// postcss.config.cjs
const { gap, gapRelative } = require('./src/functions')
module.exports = {
plugins: [
require('postcss-functions')({
functions: {
gap,
gapRelative,
},
}),
],
}
// functions.js
export const gap = (value = 1) =>
`calc(var(--gap) * ${value})`
export const gapRelative = (value = 1) =>
`calc(var(--gap--relative) * ${value})`
/* Использование: */
pre {
padding: gap() gap(0.5) gap(2);
}
Работа с Tailwind
/* globals.tailwind.css */
@theme {
--spacing: var(--gap);
/* Или: */
--spacing-0.5g: calc(var(--gap) * 0.5);
--spacing-0.75g: calc(var(--gap) * 0.75);
--spacing-1g: var(--gap);
--spacing-1.5g: calc(var(--gap) * 1.5);
--spacing-2g: calc(var(--gap) * 2);
--spacing-3g: calc(var(--gap) * 3);
--spacing-4g: calc(var(--gap) * 4);
--spacing-1gr: var(--gap--relative);
}
Работа с UnoCSS
// uno.config.ts
// Скопируйте кастомное правило:
export default defineConfig({
rules: [
[
/^(p|m|w|h|gap)(t|r|b|l|x|y|s|e|bs|be)?-([\d.]+)g(r)?$/,
(match) => {
const [, property, directive, n, relative] = match
const gapProperty = relative ? '--gap--relative' : '--gap'
const value = `calc(var(${gapProperty}) * ${n})`
const baseMap = {
p: 'padding',
m: 'margin',
w: 'width',
h: 'height',
gap: 'gap',
} as const
const base = baseMap[property as keyof typeof baseMap]
if (!base) return
const directiveMap: Record<string, string[]> = {
'': [base],
t: [`${base}-top`],
r: [`${base}-right`],
b: [`${base}-bottom`],
l: [`${base}-left`],
x:
property === 'gap'
? [base]
: [`${base}-inline-start`, `${base}-inline-end`],
y:
property === 'gap'
? [base]
: [`${base}-block-start`, `${base}-block-end`],
s: [`${base}-inline-start`],
e: [`${base}-inline-end`],
bs: [`${base}-block-start`],
be: [`${base}-block-end`],
}
const properties = directiveMap[directive ?? '']
if (!properties) return
return Object.fromEntries(properties.map((p) => [p, value]))
},
],
],
})
// Или задайте определённый набор значений:
export default defineConfig({
theme: {
spacing: {
'0.5g': 'calc(var(--gap) * 0.5)',
'0.75g': 'calc(var(--gap) * 0.75)',
'1g': 'var(--gap)',
'1.5g': 'calc(var(--gap) * 1.5)',
'2g': 'calc(var(--gap) * 2)',
'3g': 'calc(var(--gap) * 3)',
'4g': 'calc(var(--gap) * 4)',
'1gr': 'var(--gap--relative)',
},
},
})
<!-- Использование: -->
<pre class="p-1g -m-0.5g mt-0.5g mbs-2gr gap-1g"></pre>