Перейти к содержимому
Плотность
Тема

Раскладка

Оболочка приложения

Каркас экрана: боковая колонка, шапка, рабочая область. Каждая часть прокручивается сама — боковая навигация не уезжает, пока читают таблицу.

Компонент

Прогоны

Разметка
<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-endvar(--pad-panel)Нижний отступ области прокрутки. Объявляет оболочка, вычитает липкая полоса действий
--aside-w232px
--pad-panelvar(--space-5)
--space-36px
--space-48px
--space-512px
--gap-inlinevar(--space-4)
--hairline1px
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))
--surface-sunkenlight-dark(var(--n-2), var(--n-14))
--surface-pagelight-dark(var(--n-1), var(--n-13))
--surface-raisedlight-dark(var(--n-0), var(--n-12))
--radius-md7px
--text-sm0.8125rem
--text-lg1.0625rem
--control-h-md32px
--tracking-tight-0.01em
--text-primarylight-dark(var(--n-12), var(--n-1))
--z-modal1000

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument