Grid-парсер

Уникальная система описания CSS Grid раскладок через компактный строковый формат. JS-рантайм автоматически парсит атрибуты и генерирует CSS на лету.

Две дорожки, одна строка

Одна и та же строка a3b4-c2d5 раскладывается двумя независимыми способами, и оба входят в griffincss-core. Выбор — ваш, не библиотеки.

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

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

<div data-gr-layout="a3b4-c2d5">
  <div class="gr-area-a">A</div>
  …
</div>

Компилтайм — миксин в SCSS. Ни строки JS на странице, имя класса задаёте вы.

@use 'griffincss-core/scss/grid-parser' as p;

@include p.gr-grid-layout('a3b4-c2d5', $name: 'page');

<div class="gr-l-page">
  <div class="gr-area-a">A</div>
  …
</div>

Обе дают один и тот же CSS — это держит тест, сверяющий их вывод побайтово. Разница одна: без JS раскладка из миксина есть, из атрибута — нет, и контент при этом виден — защиту от FOUC ставит сам рантайм, в статическом CSS её нет. Миксин разобран ниже, в разделе «SCSS-миксины», а что выбрать под свою задачу — и нужна ли строка вообще — в разделе «Когда раскладка строкой окупается».

Формат раскладки

Раскладка описывается строкой, где - разделяет ряды, буквы задают имена grid-областей, а цифры — количество повторений. Символ _ создаёт пустую ячейку.

ЭлементНазначениеПример
-Разделитель рядовa3b4-c2d5 → 2 ряда
БуквыИмя grid-области: начинается с латинской буквы, дальше буквы и _hdr, main, side_a
ЦифрыКоличество повторений предыдущего имениa3a a a
_Пустая ячейка (в CSS: .)_3. . .

Имя читается до первой цифры, поэтому abcd — одна область с именем abcd, а не четыре. Четыре области в ряд — a1b1c1d1: цифра закрывает имя. Так и записываются многобуквенные имена — hdr4-main4; одна буква без цифры допустима только последней в ряду (a3b — область b в одну ячейку).

Раскладка проверяется до генерации CSS: счётчик без имени, нулевой счётчик, счётчик больше 64, имя длиннее 32 символов, недопустимые символы и пустой ряд отвергаются. Такой контейнер пропускается целиком, а в консоль уходит Griffincss: … — полный список правил на странице JS-рантайм.

Базовое использование

<div data-gr-layout="a3b4-c2d5">
  <div class="gr-area-a">A</div>
  <div class="gr-area-b">B</div>
  <div class="gr-area-c">C</div>
  <div class="gr-area-d">D</div>
</div>
Результат: a3b4-c2d5
A (3 колонки)
B (4 колонки)
C (2 колонки)
D (5 колонок)

Сгенерированный CSS:

/* класс .gr-l-<хеш> рантайм вешает на контейнер сам */
.gr-l-k7gi9w {
  display: grid;
  grid-template-areas: "a a a b b b b" "c c d d d d d";
  grid-template-columns: var(--gr-l-cols, repeat(7, minmax(0, 1fr)));
  grid-template-rows: var(--gr-l-rows, repeat(2, auto));
}
.gr-area-a { grid-area: a; }
.gr-area-b { grid-area: b; }
.gr-area-c { grid-area: c; }
.gr-area-d { grid-area: d; }

gap в этом правиле не пишется: отступ раскладкам даёт одно статическое правило [class*="gr-l-"] из griffincss-core.css. Поэтому модификаторы .gr-gap-sm / .gr-gap / .gr-gap-lg перекрывают его на обычных правах — см. Grid-утилиты.

Именованные области

Имена могут состоять из нескольких букв — используйте осмысленные названия для семантических областей страницы.

<div data-gr-layout="hdr4-main4-side2_2-ftr4">
  <div class="gr-area-hdr">Header</div>
  <div class="gr-area-main">Main Content</div>
  <div class="gr-area-side">Sidebar</div>
  <div class="gr-area-ftr">Footer</div>
</div>
Результат: hdr4-main4-side2_2-ftr4
Header (hdr × 4)
Main Content (main × 4)
Sidebar (side × 2)
Footer (ftr × 4)

Пустые ячейки

Символ _ создаёт пустую ячейку (преобразуется в . в CSS). _3 — три пустых ячейки подряд. Полезно для создания асимметричных раскладок и отступов.

<div data-gr-layout="hdr6-nav2hero4-news3_3-ftr6">
  <div class="gr-area-hdr">Header</div>
  <div class="gr-area-nav">Nav</div>
  <div class="gr-area-hero">Hero</div>
  <div class="gr-area-news">News</div>
  <div class="gr-area-ftr">Footer</div>
</div>
Результат: hdr6-nav2hero4-news3_3-ftr6
Header (6 колонок)
Nav (2 колонки)
Hero (4 колонки)
News (3 колонки)
Footer (6 колонок)

Совет: пустые ячейки _ создают настоящие CSS Grid ячейки с . в grid-template-areas. Они занимают место в сетке и влияют на распределение колонок.

Многострочные области (прямоугольники)

Область может занимать несколько рядов, если она повторяется с одинаковым количеством колонок. Это соответствует требованию CSS Grid: именованная область должна быть прямоугольной.

<div data-gr-layout="hero3nav3-hero3side3-ftr6">
  <div class="gr-area-hero">Hero (2 ряда × 3 колонки)</div>
  <div class="gr-area-nav">Nav</div>
  <div class="gr-area-side">Sidebar</div>
  <div class="gr-area-ftr">Footer</div>
</div>
Результат: hero3nav3-hero3side3-ftr6
Hero (2 ряда × 3 кол)
Nav (3 кол)
Sidebar (3 кол)
Footer (6 кол)

Важно: область на нескольких строках должна занимать одинаковые колонки во всех рядах (прямоугольник). Это фундаментальное ограничение CSS Grid. L-образные и T-образные формы недопустимы.

Ширины треков

Строка раскладки описывает области, а не ширины: a3b4 значит «три доли одному, четыре другому», и по умолчанию все семь дорожек равны. Это её единственный довод — читать её должно быть легче, чем grid-template-areas. Ширины задаются там, где им и место, — в CSS, двумя свойствами:

СвойствоЗначение по умолчанию
--gr-l-colsrepeat(число колонок, minmax(0, 1fr))
--gr-l-rowsrepeat(число рядов, auto)

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

<!-- Фиксированная боковая колонка и резиновая основная -->
<div data-gr-layout="a1b1" style="--gr-l-cols: 12rem 1fr">
  <div class="gr-area-a">Меню</div>
  <div class="gr-area-b">Содержимое</div>
</div>
--gr-l-cols: 12rem 1fr
A (12rem)
B (остаток)
<!-- Колонка с вилкой ширины: не уже 12rem и не шире 20rem -->
<div data-gr-layout="a1b1" style="--gr-l-cols: minmax(12rem, 20rem) 1fr">…</div>
--gr-l-cols: minmax(12rem, 20rem) 1fr
A (12–20rem)
B (остаток)
<!-- Ряды: шапка и подвал по содержимому, середина забирает остаток -->
<div data-gr-layout="a1-b1-c1" style="--gr-l-rows: auto 1fr auto; height: 12rem">…</div>
--gr-l-rows: auto 1fr auto
Шапка
Середина (остаток)
Подвал

У миксина то же самое — свойство пишется рядом с классом, который он завёл:

@include gr.gr-grid-layout('a1b1', $name: 'shell');

.gr-l-shell {
  --gr-l-cols: 12rem 1fr;
}

Число треков обязано совпадать со строкой раскладки. В a3b4 семь колонок, значит и в --gr-l-cols их семь. Меньше — недостающие браузер дорисует неявными дорожками по auto, больше — лишние останутся пустыми. Молчания здесь нет: раскладка ломается видимо, а вычисленное значение grid-template-columns показывает DevTools. Сторожа в рантайме нет намеренно — он читал бы вычисленные стили каждого контейнера, и эта цена выше цены ошибки.

Значение по умолчанию — minmax(0, 1fr), а не 1fr: доля не бывает уже своего содержимого, и одно длинное слово или широкая таблица раздували бы дорожку, вынося сетку за контейнер. С нулевым минимумом содержимое прокручивается внутри дорожки, а раскладка стоит.

Адаптивные раскладки

Атрибуты data-gr-layout-{sm,md,lg,xl} задают раскладку для соответствующего брейкпоинта. JS-рантайм генерирует непересекающиеся @media-диапазоны. Элементы, чьи grid-area отсутствуют в текущем шаблоне, автоматически скрываются.

Скрытие — по замыслу, но не по ошибке. Область, которой нет в части наборов, — обычный приём: сайдбар есть на десктопе и спрятан на телефоне. Область, которой нет ни в одном наборе контейнера, — ошибка разметки (чаще всего опечатка в имени или abcd вместо a1b1c1d1): ребёнок с таким gr-area-* исчез бы на любой ширине молча, поэтому рантайм пишет в консоль Griffincss: area "…" is not in any layout of its container — один раз на контейнер и имя.

<div data-gr-layout="a1b1"
     data-gr-layout-md="a3b4-c2d5"
     data-gr-layout-lg="hdr4-main4-side2_2-ftr4">
  <div class="gr-area-a">A</div>
  <div class="gr-area-b">B</div>
  <div class="gr-area-c">C</div>
  <div class="gr-area-d">D</div>
  <div class="gr-area-hdr">Header</div>
  <div class="gr-area-main">Main</div>
  <div class="gr-area-side">Sidebar</div>
  <div class="gr-area-ftr">Footer</div>
</div>
Результат (измените ширину окна)
A
B
C
D
Header
Main
Sidebar
Footer

Брейкпоинты по умолчанию (переопределяются через CSS-переменные):

БрейкпоинтАтрибутЗначениеCSS-переменная
Базовыйdata-gr-layout
smdata-gr-layout-sm640px--gr-bp-sm
mddata-gr-layout-md768px--gr-bp-md
lgdata-gr-layout-lg1024px--gr-bp-lg
xldata-gr-layout-xl1280px--gr-bp-xl

Те же брейкпоинты доступны и по ширине контейнера, а не окна — атрибуты data-gr-layout-c{sm,md,lg,xl}. Разница в одну букву, диапазоны строятся так же, а вместо @media рантайм пишет @container. Подробности и оговорки — на странице «Контейнерные запросы».

Непересекающиеся диапазоны: в отличие от простого накопления min-width, griffincss описывает каждый брейкпоинт диапазоном в range-синтаксисе: (width < 768px), (768px <= width < 1024px), (width >= 1024px). Правило базовой раскладки действует до первой адаптивной, промежуточные — в своих диапазонах, а последняя — от своего значения и выше. Арифметики -1px нет, поэтому щелей на дробных ширинах не остаётся, а единицы измерения брейкпоинта могут быть любыми. Границы считаются только по тем атрибутам, которые есть на элементе: если задан один data-gr-layout-lg, база действует до 1024px, а не до 640px.

Авто-присвоение grid-area

Если дочерним элементам не указан класс gr-area-*, JS-рантайм автоматически присваивает имена областей в порядке документа. Имена берутся из базового data-gr-layout (без суффикса брейкпоинта) в порядке первого появления в раскладке. Имена, уже проставленные детям вручную, из раздачи исключаются, поэтому дубликаты невозможны — даже если область занимает несколько рядов. Подробности — на странице JS-рантайм.

<div data-gr-layout="a2b2c2-a2d2e2">
  <div>A — 2×2 на 2 ряда</div>
  <div>Карточка B</div>
  <div>Карточка C</div>
  <div>Карточка D</div>
  <div>Карточка E</div>
</div>
Результат: без единого gr-area-* класса
A — 2×2 (2 ряда)

Карточка B

Ряд 1, колонки 3–4

Карточка C

Ряд 1, колонки 5–6

Карточка D

Ряд 2, колонки 3–4

Карточка E

Ряд 2, колонки 5–6

Вложенные раскладки

Grid-контейнеры можно вкладывать друг в друга без ограничений. Каждый контейнер изолирует свои grid-area имена — конфликтов между родителем и потомком не возникает. JS-рантайм генерирует каждое CSS-правило однократно, независимо от количества повторений.

<div data-gr-layout="a3b4-c2d5">
  <div class="gr-area-a">A</div>
  <div class="gr-area-b">
    <div data-gr-layout="a1c3">
      <div class="gr-area-a">Вложенный A</div>
      <div class="gr-area-c">Вложенный C</div>
    </div>
  </div>
  <div class="gr-area-c">C</div>
  <div class="gr-area-d">D</div>
</div>
Результат: вложенные раскладки
A — верхний уровень
a (влож., 1 кол.)
c (влож., 3 кол.)
C — верхний уровень
D — верхний уровень

SCSS-миксины (компилтайм)

Альтернативный способ — сгенерировать раскладки на этапе компиляции SCSS через миксины. Это не требует JS-рантайма, но раскладки фиксируются в CSS.

@use 'griffincss-core/scss/grid-parser' as p;

// Один набор раскладок = один класс. Имя задаёте вы:
@include p.gr-grid-layout(
  'a3b4-c2d5',
  $md: 'hdr4-main4-side2_2-ftr4',
  $lg: 'hero3nav3-hero3side3-ftr6',
  $name: 'page'
);
<div class="gr-l-page">…</div>

Миксин генерирует тот же CSS, что и рантайм: непересекающиеся диапазоны в range-синтаксисе, grid-template-areas, классы gr-area-* и правила авто-скрытия. Разница одна: рантайм берёт имя класса из хеша набора (.gr-l-<хеш>), а компилтайм — из $name. Раздачу gr-area-* детям компилтайм не делает — проставьте классы в разметке.

Изменение в v0.7.0: миксины gr-grid-layout-sm/md/lg/xl удалены, а gr-grid-layout() больше не принимает список раскладок — теперь это один вызов на контейнер с аргументами $sm, $md, $lg, $xl и обязательным $name. Правила больше не привязаны к значению атрибута data-gr-layout.

Когда раскладка строкой окупается

Строка окупается там, где раскладка — переменная, а не константа. Если она известна заранее и больше не меняется, честнее обычный grid-template-areas или компилтайм-миксин: ни JS, ни посредника. Ниже — четыре случая, в которых раскладка становится переменной сама собой.

Раскладку выбирают данные, а не разработчик

Случай, ради которого рантайм и нужен: раскладка приходит строкой из базы, из поля конфига или из выбора редактора в админке. Обычным CSS это не решается, и вот почему.

СпособЧем ломается
Объявить все варианты классами заранее Ограничивает редактора набором, который вы угадали
Генерировать <style> на каждую страницу Это и есть рантайм, только свой: дубликаты правил, кеш, CSP
style="grid-template-areas: …" Инлайн-стиль не умеет медиазапросы — адаптивности не будет

Последняя строка — ограничение самого CSS, а не чей-то недосмотр. Атрибут его снимает: a3b4-c2d5 — девять символов, которые кладутся в колонку таблицы, ездят в JSON и подменяются без пересборки стилей, а брейкпоинты при этом остаются на месте. Отсюда и класс задач: конструкторы страниц, дашборды с перетаскиванием плиток, раскладка под каждого арендатора в white-label, A/B-тест сменой одного атрибута.

Разметку присылает сервер

Фрагмент, пришедший по HTMX, Turbo или Livewire, несёт раскладку с собой: за data-gr-layout следит MutationObserver, и вставленный узел раскладывается наравне с тем, что был в исходном HTML. Новая раскладка не требует пересборки и выкладки CSS — она уже в присланной разметке. Механика наблюдателя — на странице «JS-рантайм», поведение под React и Vue — «Griffincss в React и Vue».

Детей вы не контролируете

Раскладка целиком живёт на родителе: детям не нужно ни классов, ни знания о ней. Это решает случай, когда разметку детей выдаёт не ваш код — вывод виджета CMS, сторонний include, цикл по неизвестному числу элементов. В чистом CSS то же самое пишется цепочкой :nth-child(), своей под каждую раскладку, и переписывается руками при первой правке. Механика — выше, в разделе «Авто-присвоение grid-area».

Одна раскладка на многих контейнерах

Правило выходит однократно на уникальный набор строк, а класс — хеш от этого набора. Пятьсот контейнеров с одной раскладкой дают одно правило CSS; инлайн-стиль дал бы пятьсот копий в разметке.

Детей больше, чем букв: раздача останавливается, когда свободные имена кончились, и лишние дети остаются без класса gr-area-*. Правило скрытия бьёт только по тем, у кого такой класс есть, — значит эти дети не прячутся, а уходят в автоматическое размещение CSS Grid, то есть в ряд под шаблоном. Там, где число блоков задаёт редактор, а не вы, это тихий неверный ответ вместо громкого отказа: считайте буквы на стороне сервера или выбирайте раскладку по числу блоков.

Когда лучше не надо

Примеры использования

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

Holy grail: каркас с боковыми колонками

Классика документаций и порталов: шапка, навигация слева, контент, оглавление справа, футер. Вся геометрия — одна строка hdr6-nav1main4toc1-ftr6. Мобильная версия ещё короче: в базовом hdr1-main1-ftr1 областей nav и toc просто нет — и рантайм скрывает эти элементы сам, без единого .gr-hidden: скрытие отсутствующих областей встроено в правила раскладки.

<div data-gr-layout="hdr1-main1-ftr1"
     data-gr-layout-lg="hdr6-nav1main4toc1-ftr6">
  <header class="gr-area-hdr">Шапка</header>
  <nav class="gr-area-nav">Навигация</nav>
  <main class="gr-area-main">Контент</main>
  <aside class="gr-area-toc">Оглавление</aside>
  <footer class="gr-area-ftr">Футер</footer>
</div>
До 1024px навигация и оглавление скрыты автоматически (сузьте окно)
Шапка
Навигация
Контент
Оглавление
Футер

Split-секции с зигзагом

Лендинговый «зигзаг» текст-картинка: у первой секции раскладка txt1img1, у следующей — img1txt1. Сторона меняется перестановкой букв в строке — не нужны ни order-классы, ни зеркальная разметка. На мобильном обе секции складываются в одинаковый столбик txt1-img1: текст всегда сверху, какой бы ни была десктопная сторона.

<section data-gr-layout="txt1-img1" data-gr-layout-md="txt1img1">
  <div class="gr-area-txt">Текст</div>
  <div class="gr-area-img">Картинка</div>
</section>

<section data-gr-layout="txt1-img1" data-gr-layout-md="img1txt1">
  <div class="gr-area-txt">Текст</div>
  <div class="gr-area-img">Картинка</div>
</section>
Сторона картинки — перестановка букв в строке раскладки

Быстрый старт

Текст слева, картинка справа.

Картинка

Гибкие темы

А здесь стороны поменялись — img1txt1.

Картинка

Бенто-сетка дашборда

Журнальная раскладка из клеток разного размера: три KPI-плитки в ряд, под ними широкий график на два ряда и колонка списка рядом. Прямоугольник графика описывается повторением области в соседних рядах — big2list1-big2list1, — а на мобильном всё это одной базовой строкой складывается в стопку.

<div data-gr-layout="a1-b1-c1-big1-list1"
     data-gr-layout-md="a1b1c1-big2list1-big2list1">
  <div class="gr-area-a">Выручка</div>
  <div class="gr-area-b">Заказы</div>
  <div class="gr-area-c">Конверсия</div>
  <div class="gr-area-big">График</div>
  <div class="gr-area-list">Последние события</div>
</div>
Клетки разного размера — одна строка: a1b1c1-big2list1-big2list1
Выручка
1,2 млн ₽
Заказы
318
Конверсия
3,4 %
График — 2 колонки × 2 ряда
Последние события