Safari и тестирование

CSS interpolate-size: allow-keywords и calc-size() для анимации интринсик-высот на статическом HTML, QA Safari и репетиции на облачном Mac

MacHTML Lab2026.04.2331 мин чтения

Статические маркетинговые сайты по-прежнему отгружают аккордеоны, раскрывающиеся панели и фильтры, которым нужно плавно вырасти от нуля до естественной высоты без таблицы пикселей на каждый брейкпоинт. Долгое время честный ответ был «измерить в JavaScript и задать пиксели», потому что CSS считал height: auto неинтерполируемым ключевым словом на этапе computed value. В 2026 связка interpolate-size: allow-keywords и calc-size() наконец даёт декларативный мост между интринсик-размерами и математикой transition — но только если вы понимаете каскад, взаимодействие с дефолтами flex/grid и то, как WebKit планирует reflow. Здесь — боль, примитивы, прогрессивное улучшение через @supports, перфоманс, доступность и матрица решений, чтобы статические бандлы оставались лёгкими, а motion — предсказуемым. Для сравнения с кросс-документными переходами читайте руководство по View Transitions для статических MPA; если внутри раскрытия есть автоматически растущие поля форм, сочетайте материал с field-sizing для статических форм, чтобы анимации высоты не боролись с интринсиком textarea.

Экономика проверки важна: воспроизвести субпиксельную вёрстку на тех же GPU, что у клиентов, дешевле на арендованном Mac mini, чем выкатить сломанную анимацию. MacHTML держит облачные Apple Silicon хосты около $16.9 в день — эту цифру мы используем как ориентир бюджета репетиций ниже.

Почему анимировать height:auto было больно

Классические transition интерполируют длины, проценты и часть transform, но height: auto особенный: used value появляется только после измерения контента. Если задать transition: height 240ms ease между 0 и auto, многие движки просто щёлкают в конце — нет пары чисел для интерполяции. В ответ придумали max-height: 9999px, что кажется гладким, пока перевод не раздует текст и длительность перестанет соответствовать реальной глубине, или пока дерево лейаута не решит, что коробка потенциально бесконечна. scrollHeight в JavaScript работает, но возвращает main-thread, усложняет CSP на статике и ломается при поздней подгрузке веб-шрифтов.

Новая модель держит измерение внутри стилей: вы всё ещё думаете интринсиком, но даёте движку числовой коридор. Это критично для статических экспортов без React и где каждый байт main.js на счету. Для команд Safari-first важно помнить, как WebKit защищался от циклических колебаний, когда процентные высоты смешивались с интринсиком во flex — понимание спецификации спасает от ложных «багов браузера».

Ещё один нюанс — overflow: hidden на скруглённых карточках: при max-height тени и outline могут расползаться относительно reveal, давая «двойную дверь» на Retina. Интринсик-интерполяция держит used height ближе к реальному content box, если не крутить лишние свойства параллельно.

interpolate-size: allow-keywords на практике

interpolate-size подключает поддерево к интерполяции ключевых слов, которые раньше были нечисловыми. На корне disclosure сообщаете движку, что переходы между интринсиком и длиной можно измерять, если остальные longhands согласны. Обычно свойство ставят на .disclosure или обёртку details, а не на весь body, иначе расширяется поверхность интерполяции и посторонние правила внезапно становятся дорогими.

.disclosure {
  interpolate-size: allow-keywords;
  overflow: clip;
  transition: height 260ms cubic-bezier(.2,.8,.2,1);
}

Сочетайте свойство с явными состояниями height: свёрнуто 0, развёрнуто может оставаться auto, но теперь движок строит числовую траекторию. При переключении классов height: 0 против height: auto следите за collapse маргинов: соседние margin могут оставить «фантомный» зазор — лучше padding на внутреннем wrapper. interpolate-size наследуется: удобно для вложенных аккордеонов, но опасно в библиотеке компонентов, если дочерняя карточка случайно наследует allow-keywords и одновременно анимирует flex-basis — Safari сделает лишние проходы layout. Ограничьте утилитой и убирайте со статических оболочек без motion.

calc-size(fit-content) как мост измерений

calc-size() оборачивает интринсик-ключевое слово в выражение, дающее длину, которую может сэмплировать transition. Типичный аккордеон: развёрнуто height: calc-size(fit-content, size), чтобы браузер измерил блок как при height: auto, а свёрнуто 0 или фиксированный заголовок — тогда интерполируются две длины, а не длина и ключевое слово.

.panel[data-open="true"] .panel-body {
  height: calc-size(fit-content, size);
}
.panel[data-open="false"] .panel-body {
  height: 0;
}

Переходящим с max-height стоит сравнить ощущаемую скорость: calc-size() следует реальной глубине текста, поэтому easing честен и для коротких ответов, и для длинных политик. Минус — чувствительность к динамике: live region, добавляющая текст во время анимации, сдвигает цель (корректно, но может тянуть easing, рассчитанный на фиксированную дистанцию). На статике замораживайте DOM до transitionend, если не проектируете mid-flight reflow.

С padding помните про box-sizing: при * { box-sizing: border-box; } убедитесь, что измеренная высота совпадает с ожидаемым border box, иначе Safari анимирует content box, а бордер «прыгнет» кадром.

Прогрессивное улучшение с @supports

Дайте слой fallback: без calc-size() оставьте мгновенное переключение или max-height clip. Тестируйте именно токен функции в @supports, а не общее свойство — частичные реализации могут парсить, но не интерполировать.

@supports (height: calc-size(fit-content, size)) {
  .panel-body { transition: height 240ms ease; }
}
@supports not (height: calc-size(fit-content, size)) {
  .panel-body { transition: max-height 320ms ease; max-height: 0; }
  .panel[data-open="true"] .panel-body { max-height: 80vh; }
}

Можно печатать результат @supports в HTML-комментарий на билде для саппорта, но не прячьте контент только за современным CSS: аккордеоны должны оставаться доступны без стилей. Motion — опциональное улучшение поверх семантики details/summary или кнопок с aria-expanded.

Сетка, flex и min-height:auto

У flex-элементов по умолчанию min-height: auto, они не сжимаются ниже интринсик-минимума. Для анимации от 0 до интринсика часто нужен min-height: 0, чтобы свёрнутое состояние было честным без overflow-спама. В grid аналогично min-size:auto и размеры дорожек: строка может отказаться схлопываться, если дети дают большой минимум.

Если disclosure внутри области с align-self: stretch, блочный размер grid-item определён даже при интринсик-контенте — это меняет разрешение calc-size(), иногда ускоряя layout, иногда вступая в конфликт с aspect-ratio. Для маркетинговых лендингов чаще берите одноколоночную сетку тела статьи и изолируйте анимированные панели с contain: layout после проверки, что фокус-кольца не клипуются.

Смешивание анимации gap и высоты заставляет Safari делать два зависимых layout кадра, если ещё крутить margin-block-end у соседей. Оставьте margin статичным на время height tween, мягкий вход сделайте opacity.

Заметки по Safari и WebKit

Движок анимаций WebKit сливает обновления стиля, если main thread занят — высота может дёрнуться даже на M3. Статика не должна блокировать, но аналитика всё равно может джанкать: откладывайте third-party ниже сгиба или за idle callback. Сравнивайте stable Safari и STP, если релиз пересекается с волной обновлений macOS, и фиксируйте пиксельные диффы.

Аппаратное ускорение не переносит height на композитор — layout остаётся. Одновременный transform: translateY() и height удваивает работу, если не поднять слой с осторожным will-change: transform. На один жест — одна примитива: либо slide transform, либо reveal по высоте.

Для субпиксельного текста держите одинаковые настройки сглаживания в свёрнутом и развёрнутом состояниях, иначе QA увидит «мерцание» из-за инвалидации кэша bitmap.

Производительность: thrash и слияние

Каждый сэмпл высоты тянет layout. Пять одновременно открытых панелей умножают стоимость. Ограничьте параллельность, слегка разнесите по времени или пометьте внеэкранные панели content-visibility: auto после проверки доступности. Смотрите фиолетовые полосы layout в таймлайне WebKit шире кадра.

Если остаётся JS fallback с ResizeObserver, не читайте layout синхронно внутри колбэка во время CSS transition — получите петлю. Троттлите через requestAnimationFrame и пропускайте записи, пока document.hidden.

Доступность и prefers-reduced-motion

Часть пользователей чувствительна к вестибулярной нагрузке. При prefers-reduced-motion: reduce сокращайте длительности почти до нуля или заменяйте на лёгкий opacity-хинт, не убирая контент и порядок DOM.

@media (prefers-reduced-motion: reduce) {
  .panel-body { transition: none !important; }
}

Клавиатурным пользователям нужны предсказуемые кольца фокуса: если панель клипует outline, используйте overflow: clip с внутренним padding или перенесите outline на внутренний wrapper. aria-expanded обновляйте синхронно со сменой состояния, а не только после конца анимации, если UX явно не требует задержки (обычно нет).

Паттерны статического HTML

Для статического экспорта держите один класс-контейнер и состояние в data-open через минимальный JS или нативные details с зеркалированием в CSS. Без JS details дают открытие/закрытие, а interpolate-size + calc-size() наконец согласуют семантику и motion. Не смешивайте две системы высоты — правила будут драться.

Слои CSS: типографика, затем layout, затем motion — так маркетинговый !important не отключит transition в релизный день. На S3/CloudFront нет фичефлагов, держите motion в отдельном motion.css?v= для контролируемого bust кэша.

Матрица: когда анимировать высоту

СценарийИнтринсик tween?Заметка
Юридический аккордеонДаcalc-size следует глубине; без max-height гадания.
Бесконечная лентаРедкоВиртуализируйте; высота ломает scroll метрики.
МодалкиИногдаВход/выход лучше transform; высота для контента.
Sticky navОсторожноКонфликт с compositing sticky; тест Safari.

Нумерованный чеклист QA

  1. Проверьте высоты 320/390/834 px с самыми длинными локалями.
  2. Включите prefers-reduced-motion в macOS и убедитесь, что состояние понятно.
  3. Откройте пять панелей подряд и следите за CPU на базовом M2.
  4. Снимите таймлайн WebKit с теми же third-party скриптами, что в проде.
  5. Проверьте порядок фокуса в середине transition внутри clip.
  6. Сравните stable Safari и STP при окне обновления macOS.
  7. Перетестируйте после изменения preload шрифтов.
  8. Снимите диффы для тёмной темы и повышенного контраста.

FAQ

max-height ещё ок?

Да как fallback, но calc-size честнее на длинных текстах.

Заменяет scroll-driven анимации?

Нет; это разные механизмы — комбинируйте с профилированием.

А width:auto?

Похожие идеи, но горизонтальный reflow текста дороже — меряйте.

Надёжный motion на статике — это репетиция на реальном Safari с прод-шрифтами, расширениями и масштабом дисплея. Mac mini от MacHTML за ~$16.9 в день даёт постоянную мишень как у клиентов, с SSH для скриншотов и VNC для дизайнеров.

Репетиция анимаций высоты на облачном Mac mini

Загрузите статический бандл в Safari на Apple Silicon, профилируйте штормы аккордеонов и подпишите CSS до merge.

Тест Safari motion на облачном Mac
От $16.9/день