Осмысленное движение — Примеры

Пример 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 });
}

Элемент, который исчезает без движения, читается как ошибка отрисовки: пользователь не может понять, удалили его, он отказал или его вовсе не было, а соседние строки смещаются без объяснения. Схлопывание на месте отвечает на оба вопроса — занимаемое им место закрывается, так что новая форма списка читаема, а растворение говорит, что изменение намеренное. Если удалять узел по завершении анимации, данные и интерфейс остаются в шаге друг с другом. Тот же шаблон покрывает и быстрый двойной клик: обработчик добавляет класс, поэтому строка, которая уже уходит, не удаляется дважды. Движение здесь несёт сообщение, которое иначе пришлось бы нести тексту.