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

Основания

Движение

Три длительности и две функции плавности. Достаточно быстро, чтобы движение читалось причиной, а не анимацией: всё, что вызвал пользователь, обязано разрешиться внутри --dur-2.

Компонент
идёт
Разметка
<div class="inst-cluster">
  <button class="inst-btn" type="button">Наведите и нажмите</button>
  <button class="inst-btn inst-btn--primary" type="button" aria-busy="true">Запустить</button>
  <span class="inst-badge" data-tone="running"><span class="inst-dot"></span>идёт</span>
</div>

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

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

.my-thing {
  transition: background var(--dur-1) var(--ease-out);
}
Что Обязательно Почему
Длительность из --dur-* да Четыре ступени. Число на месте расходится с остальным китом на первой же правке
--ease-out для появления да Вход быстрый в начале и мягкий в конце; --ease-in-out — только для того, что ходит туда-обратно
Уменьшенное движение учтено да Бесконечные индикаторы замедляются, а не гаснут: остановленный индикатор сообщает «ничего не происходит», когда происходит
Анимация у данных нет График — это состояние, а не процесс. Движется то, что меняется по действию

Когда использовать

Используйте Возьмите другое
Переход поверхности и рамки по действию пользователя Движение layout — ховер не двигает раскладку. Состояние показывает бейдж
Бесконечный индикатор для занятости машины Определённая полоса, если процент неизвестен — застрявшая на 90% полоса врёт. У неопределённой меры нет aria-valuenow
--dur-3 для заполнения меры Та же длительность для ховера — 220ms на наведении читаются как задержка интерфейса, см. кнопку
Переход, который может не доиграть Анимация как носитель смысла в одиночку — состояние обязано быть и в разметке: aria-busy, data-state. См. шаг
Появление оверлея через @starting-style Ручное открытие на JS — верхний слой и Escape даёт платформа, см. поповер

Шкала

Токен Значение Где
--dur-1 80ms Наведение, нажатие, фокус, поворот шеврона, подчёркивание ссылки. Это большинство переходов кита
--dur-2 140ms Свитч, появление и исчезновение поповера. Потолок для всего, что вызвал пользователь
--dur-3 220ms Заполнение меры — единственное, что меняет геометрию по данным, а не по касанию

Всё, что не укладывается в --dur-2, требует состояния, а не перехода: если процесс длиннее, показывать его должен индикатор занятости, а не растянутая анимация.

Функции плавности

Токен Значение Где
--ease-out cubic-bezier(0.22, 0.61, 0.36, 1) Все переходы. Резкий старт и мягкая остановка читаются как отклик на действие
--ease-in-out cubic-bezier(0.65, 0, 0.35, 1) Бесконечные циклы: пульсация точки, дыхание скелетона, неопределённая мера

Линейная остаётся там, где вращение обязано быть равномерным. Кольцо спиннера и кольцо занятой кнопки крутятся linear: замедление в цикле читается как рывок.

Поведение

Что Длительность Как
Наведение и нажатие кнопки --dur-1 background, border-color, transform
Нажатие --dur-1 scale(0.985) — нажатие надо почувствовать, а не разглядывать
Фокус Кольцо появляется мгновенно: задержка на фокусе читается как лаг
Шеврон селекта и раскрытого шага --dur-1 rotate
Свитч --dur-2 Положение бегунка и заливка дорожки
Поповер --dur-2 opacity + translate, плюс display и overlay через allow-discrete
Заполнение меры --dur-3 inline-size
Точка running 1.6s, бесконечно Пульсация прозрачности
Спиннер и кольцо занятой кнопки 0.7s, бесконечно Вращение
Скелетон 1.6s, бесконечно Дыхание прозрачностью, а не блик: градиентов кит не знает
Неопределённая мера 1.3s, бесконечно Проход отрезка по дорожке
Каретка стриминга 1s, бесконечно Мигание шагами

Уменьшенное движение

prefers-reduced-motion: reduce обрабатывается в отдельном каскадном слое, объявленном последним. Слой нужен, чтобы правило перебивало компоненты (у .inst-btn специфичность выше, чем у *) и при этом не перебивало приложение: без слоя это требовало бы !important, а !important внутри слоя выигрывает у неважных объявлений приложения — то есть кит нарушал бы собственное обещание.

Переходы схлопываются

*, *::before, *::after {
  transition-duration: 0.01ms;
  animation-delay: 0ms;
  scroll-behavior: auto;
}

0.01ms, не none. Событие transitionend продолжает приходить, и машины состояний приложения, которые его слушают, продолжают работать. Выключенный переход тихо остановил бы их.

Индикаторы не гаснут

Здесь кит расходится с общепринятым «выключить всё».

Бесконечную анимацию нельзя схлопнуть. Сжатая до 0.01ms анимация с одной итерацией — это не сжатие длительности, это остановка: спиннер замирает в случайной фазе, точка перестаёт пульсировать, каретка застывает. Кит, вся работа которого — показывать, что машина занята, переставал бы показывать, что машина занята. У занятой кнопки это буквально превращает её в пустой прямоугольник: подпись погашена, а кольцо стоит.

Поэтому индикаторы уходят на частоту, которая читается как «идёт», но не дёргает периферийное зрение. Укачивают амплитуда и частота, а не движение само по себе.

Индикатор Обычно При reduce
.inst-spinner 0.7s 2.4s
.inst-btn[aria-busy="true"] 0.7s 2.4s
.inst-skeleton 1.6s 3s
.inst-meter в неопределённом состоянии 1.3s 3s
.inst-dot в тоне running 1.6s 3s
.inst-dot внутри .inst-task и .inst-step в состоянии running 1.6s 3s
.inst-caret 1s Мигание снято, каретка остаётся видимой

У каретки мигание говорит только о факте, не о скорости. Поэтому гасится мигание, а сама каретка остаётся.

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

Доступность

Уменьшенное движение Переходы схлопываются до 0.01ms; индикаторы активности замедляются, а не останавливаются
Занятость Озвучивается aria-busy, а не анимацией. Движение — второй носитель, не единственный
Прогресс Неопределённая мера не имеет aria-valuenow, и это само по себе сообщение
Фокус Не анимируется: кольцо появляется сразу
Мигание Ничто в ките не мигает чаще 3 Гц
Слой kit.motion объявлен последним, поэтому перебивает компоненты и не перебивает приложение — без единого !important

API

ИмяЗначениеЧто делает
токен
--dur-180ms
--dur-2140ms
--dur-3220ms
--ease-outcubic-bezier(0.22, 0.61, 0.36, 1)
--ease-in-outcubic-bezier(0.65, 0, 0.35, 1)

Что кит не анимирует

Не анимируется Почему
Раскладка на наведении Ховер не двигает layout, не меняет кегль и не добавляет тень
Появление контента при прокрутке Инструментальный интерфейс читают, а не смотрят
scroll-behavior: smooth глобально Воюет с навигацией с клавиатуры и делает длинные логи вязкими. Включается точечно, на конкретное взаимодействие
Цвет текста при смене темы Переключение темы — не переход, а другое состояние документа

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