Осмысленное движение — Примеры
Пример 1 — Панель, которая в обоих направлениях длится 400 мс
Боковая панель использует один и тот же долгий переход и на открытие, и на закрытие, поэтому закрытие ощущается как зависание интерфейса. Скажите, какая половина движения неправильна, и задайте время для двух направлений отдельно.
.drawer {
transform: translateX(100%);
transition: transform 400ms ease-in-out;
}
.drawer.is-open { transform: translateX(0); }
Решение
.drawer {
transform: translateX(100%);
transition: transform 160ms ease-out; /* вход: время прибыть */
}
.drawer.is-closing {
transition-duration: 110ms; /* выход: примерно две трети */
}
.drawer.is-open { transform: translateX(0); }
Вся суть здесь в длительности, а воспринимаемая скорость асимметрична: элемент может позволить себе прибывать не спеша и не ощущаться медленным, тогда как всё, что задерживается на выходе, блокирует следующее действие, поэтому выходу и достаётся более короткая половина. Панель, принадлежащая правому краю, входит справа — и именно это делает движение информацией, а не украшением, ведь направление сообщает, откуда она пришла. Диапазон от 100 мс до 250 мс покрывает почти всё в интерфейсе, причём верх оставлен для крупных перемещений вроде полноэкранного оверлея, а низ — для малых изменений состояния. Границу этого диапазона решают, сравнивая три длительности друг с другом, а не угадывают.
Пример 2 — Строка, которая исчезает, будто это ошибка
Удаление мгновенно убирает элемент из интерфейса, и строки ниже прыгают вверх. Скажите, в чём пользователь остаётся неуверен, и заставьте удаление объяснить себя.
function removeRow(id) {
rows = rows.filter((row) => row.id !== id);
render(rows); // строка просто исчезает; список прыгает
}
Решение
.row.is-removing {
overflow: hidden;
transition: height 140ms ease-out, opacity 140ms ease-out;
height: 0;
opacity: 0;
}
function removeRow(id) {
const el = document.querySelector(`[data-row="${id}"]`);
el.classList.add("is-removing");
el.addEventListener("transitionend", () => {
rows = rows.filter((row) => row.id !== id);
render(rows);
}, { once: true });
}
Элемент, который исчезает без движения, читается как ошибка отрисовки: пользователь не может понять, удалили его, он отказал или его вовсе не было, а соседние строки смещаются без объяснения. Схлопывание на месте отвечает на оба вопроса — занимаемое им место закрывается, так что новая форма списка читаема, а растворение говорит, что изменение намеренное. Если удалять узел по завершении анимации, данные и интерфейс остаются в шаге друг с другом. Тот же шаблон покрывает и быстрый двойной клик: обработчик добавляет класс, поэтому строка, которая уже уходит, не удаляется дважды. Движение здесь несёт сообщение, которое иначе пришлось бы нести тексту.