Раскладка
Оболочка приложения
Каркас экрана: боковая колонка, шапка, рабочая область. Каждая часть прокручивается сама — боковая навигация не уезжает, пока читают таблицу.
Прогоны
Разметка
<div class="inst-shell">
<aside class="inst-shell-aside">
<a class="inst-shell-brand" href="#"><span class="inst-dot" data-tone="ok"></span>instrument</a>
<nav class="inst-nav" aria-label="Разделы">
<div class="inst-nav-group">
<span class="inst-nav-label">Работа</span>
<a class="inst-nav-item" href="#" aria-current="page">Прогоны
<span class="inst-badge inst-nav-count">7</span></a>
<a class="inst-nav-item" href="#">Очередь</a>
</div>
</nav>
</aside>
<header class="inst-shell-header">
<nav aria-label="Хлебные крошки">
<ol class="inst-crumbs">
<li><a href="#">Проекты</a></li>
<li><span aria-current="page">Прогон #4127</span></li>
</ol>
</nav>
<span class="inst-cluster-spacer"></span>
<button class="inst-btn inst-btn--sm" type="button">Экспорт</button>
<button class="inst-btn inst-btn--sm inst-btn--primary" type="button">Запустить</button>
</header>
<main class="inst-shell-main" id="main">
<div class="inst-container inst-container--full inst-stack">
<div class="inst-page-header">
<div class="inst-page-header-main">
<h1 class="inst-page-title">Прогоны</h1>
</div>
</div>
</div>
</main>
</div>
Использование
<a class="inst-skip-link" href="#main">К содержанию</a>
<div class="inst-shell">
<aside class="inst-shell-aside">
<a class="inst-shell-brand" href="/">Приложение</a>
…
</aside>
<header class="inst-shell-header">…</header>
<main class="inst-shell-main" id="main">…</main>
</div>
Порядок в разметке — aside, header, main; на экране их расставляет
grid-template-areas, поэтому шапка стоит справа от колонки, а не над ней. На
узком экране порядок областей меняется тем же свойством — без второго набора
разметки.
Пропуск к содержимому стоит до оболочки: это первое, что встречает
клавиатура, и href обязан указывать на id рабочей области.
| Что | Почему |
|---|---|
inst-skip-link первым в <body> |
Иначе клавиатура каждый раз проходит всю навигацию заново |
id на inst-shell-main |
Цель пропуска |
<aside> / <header> / <main> |
Ориентиры страницы. <div> их не даёт |
aria-label у каждой <nav> внутри |
В оболочке их обычно три: разделы, крошки, вкладки |
Ширина колонки перенастраивается одной строкой:
.my-app { --aside-w: 280px; }
Когда использовать
| Используйте | Возьмите другое |
|---|---|
| Каркас всего приложения: одна оболочка на документ | Внутренняя двухколоночная раскладка — сплит: он переносится сам и не занимает всю высоту окна |
| Нужны независимо прокручиваемые области | Ограничение ширины содержимого — контейнер внутри рабочей области |
| Постоянная боковая навигация — nav | Область приложения внутри экрана — панель |
| — | Вертикальный ритм внутри области — примитивы потока |
Устройство
| Область | Что делает |
|---|---|
inst-shell-aside |
Боковая колонка шириной --aside-w. Прокручивается сама, лежит на --surface-sunken |
inst-shell-header |
Шапка. Ряд с обязательным переносом: крошки и контролы на узком экране уходят на вторую строку, а не выталкивают документ за вьюпорт |
inst-shell-main |
Рабочая область. Прокручивается сама, даёт вертикальный воздух и объявлена контейнером app |
Горизонтальный воздух даёт не рабочая область, а контейнер внутри неё: у них разная работа — один ограничивает меру чтения, другая держит прокрутку.
Заголовок колонки
Имя приложения — первый ребёнок колонки, до навигации.
Класс живёт в ките из-за одной строки — отбивки снизу. В колонке она обязательна: без неё имя встаёт в ряд с пунктами и делается первым из них. В ряду (узкий экран) она же смещает имя вверх на половину себя, и логотип с пунктами перестают стоять на одной линии. Расхождение в шесть пикселей, которое видно сразу и не находится никогда — потому что искать его идут в выравнивании, а лежит оно в отбивке.
Липкая полоса действий
Рабочая область объявляет свой нижний отступ переменной --scroll-pad-end.
position: sticky закрепляется относительно поля паддинга области
прокрутки, а не её границы. Полоса действий, закреплённая по inset-block-end: 0, останавливается на высоту отступа выше видимого низа — и под ней остаётся
живая щель, сквозь которую видно, как едет содержимое. Полоса перестаёт быть
дном.
Знает про этот отступ только тот, кто его поставил. Отсюда переменная:
полоса действий вычитает её сама. Своя область
прокрутки — объявите --scroll-pad-end на ней.
Поведение
@media (width < 60rem) { … }
Единственный порог во всём ките. Оболочка и есть то единственное, что
действительно зависит от размера окна: всё остальное перестраивается интринсиком
или отвечает на ширину своей области через @container — см.
конституцию.
Ниже 60rem есть два поведения, и выбор между ними не вкусовой, а по длине навигации.
Умолчание: горизонтальная полоса
Колонка становится лентой над содержимым:
| Что меняется | Как |
|---|---|
| Порядок областей | header, aside, main — колонкой |
| Колонка | Ряд с горизонтальной прокруткой, нижняя линия вместо боковой |
inst-nav, inst-nav-group |
Разворачиваются в ряд |
inst-nav-label |
Скрывается: подписи групп в полосе не помещаются |
inst-nav-item |
Перестаёт переносить подпись |
Работает без единой строки JS и потому не может «не открыться»: у приложения, которое ничего не подключило, навигация на телефоне всё равно есть. Годится примерно до восьми пунктов.
Вариант: выезжающий ящик
<aside class="inst-shell-aside inst-shell-aside--drawer">…</aside>
<button class="inst-btn inst-btn--icon inst-shell-nav-toggle" type="button"
aria-label="Разделы" aria-expanded="false" aria-controls="sidebar">…</button>
Как только пунктов больше восьми, лента перестаёт работать: семьдесят один пункт — это лента шириной почти восемь тысяч пикселей без заголовков групп. Это уже не «неудобно», это отсутствие навигации.
Ящик сохраняет вертикаль и подписи групп, выезжает из-за начального края и
гасит фон той же подложкой --scrim, что и модалка.
| Класс | Работа |
|---|---|
inst-shell-aside--drawer |
Колонка уходит в фиксированный слой и выезжает по требованию |
inst-shell-nav-toggle |
Кнопка вызова. На широком экране скрыта: колонка и так на месте, и кнопка была бы обманом |
Что даёт приложение. Одну строку — переключить aria-expanded на кнопке.
Ровно тот же контракт, что у модалки с
showModal(). Ловушка фокуса, Escape и клик по подложке — тоже слой
приложения: кит рисует состояние, но не выдумывает его.
Сценарии
Оболочка сама по себе — три пустые области. Работу видно, когда в них что-то стоит, и главное в ней не видно на картинке вовсе: каждая область прокручивается сама. Прокрутите таблицу — колонка и шапка останутся на месте.
Собранные экраны на этой оболочке лежат в «Сборках»: дашборд, инспектор, экран настроек. Там же видно, как в неё садятся навигация, шапка экрана и секции.
Правила
Так Пропуск к содержимому первым в body
Иначе клавиатура каждый раз проходит всю навигацию заново.
Не так display: none на пропуске
Скрытое так выпадает из порядка обхода. Кит уводит его в клип и возвращает по
:focus-visible.
Так Одна оболочка на документ
Она и есть каркас. Внутренняя двухколоночная раскладка — сплит.
Не так Лента навигации при семидесяти пунктах
Это лента шириной в восемь тысяч пикселей без заголовков групп, то есть отсутствие навигации. От восьми пунктов — ящик.
Доступность
| Пропуск к содержимому | Первый элемент в порядке обхода. Скрыт до фокуса clip-path, а не display: none: скрытое так остаётся фокусируемым |
| Ориентиры | <aside>, <header>, <main> дают навигацию по ориентирам без единого role |
| Прокрутка | Три независимые области с overflow: auto. overscroll-behavior не ставится: контейнеру, которому нечего прокручивать, contain съедает колесо мыши |
| Высота | 100dvh, а не 100vh — на мобильных панель браузера иначе срезает нижнюю часть рабочей области |
| Перенос в шапке | Обязателен: без него узкий экран получает горизонтальную прокрутку документа, и 100dvh перестаёт помещаться по высоте |
| Печать | Боковая колонка скрывается, оболочка разворачивается в обычный поток, прокручиваемые области печатаются целиком |
API
| Имя | Значение | Что делает |
|---|---|---|
| класс | ||
inst-shell | — | Грид на всю высоту окна (100dvh) |
inst-shell-aside | — | Боковая колонка |
inst-shell-brand | — | Имя приложения над разделами. Первым ребёнком колонки |
inst-shell-header | — | Шапка |
inst-shell-main | — | Рабочая область. Контейнер app |
inst-skip-link | — | Пропуск к содержимому. Виден только по :focus-visible |
| токен | ||
--scroll-pad-end | var(--pad-panel) | Нижний отступ области прокрутки. Объявляет оболочка, вычитает липкая полоса действий |
--aside-w | 232px | |
--pad-panel | var(--space-5) | |
--space-3 | 6px | |
--space-4 | 8px | |
--space-5 | 12px | |
--gap-inline | var(--space-4) | |
--hairline | 1px | |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | |
--surface-sunken | light-dark(var(--n-2), var(--n-14)) | |
--surface-page | light-dark(var(--n-1), var(--n-13)) | |
--surface-raised | light-dark(var(--n-0), var(--n-12)) | |
--radius-md | 7px | |
--text-sm | 0.8125rem | |
--text-lg | 1.0625rem | |
--control-h-md | 32px | |
--tracking-tight | -0.01em | |
--text-primary | light-dark(var(--n-12), var(--n-1)) | |
--z-modal | 1000 | |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument