Адаптивное видео с помощью встроенных математических функций CSS

Когда я в твиттере спросила для чего могут быть нужны в CSS типизированные значения атрибутов, Вадим Макеев предположил, что это было бы очень удобно для адаптивных картинок.

Но я подумала, что картинки как-то умеют при ресайзе изменять высоту, сохраняя пропорции, а вот фреймы — совсем нет. При этом на адаптивных сайтах важно иметь возможность подгонять под ширину окна любой контент, и видео тоже. Что тут можно сделать?

Для решения этой задачи существуют разные способы: дополнительная обёртка с паддингом или варианты с JS, но простого изящного решения на чистом CSS до сих пор нет. В черновиках W3C для этих целей есть свойство aspect-ratio, подробнее про него можно почитать в статье Designing An Aspect Ratio Unit For CSS, но на данный момент оно не поддерживается ни одним браузером (демо для потестить).

Пока копалась в математических функциях, пришло в голову, что aspect-ratio можно попробовать воспроизвести с помощью этих функций и кастомных свойств. Крис Койер предлагал использовать кастомные свойства и calc() для вычисления паддинга в способе с обёрткой:

<div class="videoWrapper" style="--aspect-ratio: 3 / 4;">
  <iframe ...>
.videoWrapper {
  ...
  /* falls back to 16/9, but otherwise uses ratio from HTML */
  padding-bottom: calc(var(--aspect-ratio, .5625) * 100%);
}

Но что если получать соотношения сторон из ширины и высоты, а потом на основе этого динамически вычислять высоту самого фрейма?

Мне потребовалось некоторое время, чтобы понять как написать работающий код. Одной из проблем было то, что, если поделить друг на друга значения с одинаковыми единицами измерения, calc() возвращает 0, а не соотношение. Следовательно, чтобы получить соотношение сторон, нужно использовать именно числа без единиц измерения.

После этого всё стало просто: в атрибутах элемента, который нужно ресайзить, в кастомных свойствах указываются размеры элемента без единиц измерения:

<iframe
    width="560" height="315"
    style="--width: 560; --height: 315;"
    class="video"
    ...
></iframe>

А в CSS добавляется немного математики с использованием этих свойств:

.video {
  /* Находим соотношение сторон */
  --index: calc(var(--height) / var(--width));
  /* Добавляем пиксели к высоте */
  --height-with-units: calc(var(--height) * 1px);

  /* Запрещаем фрейму растягиваться шире родителя */
  max-width: 100%;

  /* Выбираем минимальное значение из исходной высоты
    и вычисленной на основе ширины вьюпорта  */
  height: min(calc(100vw * var(--index)), var(--height-with-units));
}

Посжимайте демо, чтобы увидеть как это работает, или откройте в отдельной вкладке и попробуйте там.

Когда фрейм упрётся в края окна и начнёт сужаться вместе с ним, ширина фрейма будет равна ширине вьюпорта (100vw), следовательно, её можно использовать для вычисления высоты. Полученная таким образом высота окажется меньше исходной, и в min() выберется именно она. При растягивании окна высота, вычисленная на основе ширины вьюпорта, станет больше исходной высоты фрейма, поэтому в min() выиграет исходная высота, и фрейм не будет тянуться за пределы исходной высоты.

Возможно, не для всех случаев ширина фрейма будет равна ширине окна, и в некоторых случаях может потребоваться поменять 100vw на другое значение. Например, в демо ширина видео будет calc(100vw - 2rem), но в рассчётах это не учтено, а поправлять я не стала, чтобы не усложнять понимание логики в min().

Также следует помнить, что при использовании препроцессоров они попытаются вычислить min() внутри себя, но не смогут этого сделать из-за несовместимых единиц. Почитать о проблеме можно в статье Аны Тюдор When Sass and New CSS Features Collide. Чтобы SCSS проигнорировал min(), название функции достаточно написать с заглавной буквы: Min(...) (как сделано в коде демо), а в Less — обернуть кавычками: ~"min(...)".

Было бы здорово не дублировать размеры в кастомных свойствах, а брать из атрибутов, но в данный момент это не поддерживается ни одним браузером, хотя в том же aspect-ratio предполагается использовать именно значения размеров из атрибутов ширины и высоты.

Однажды наступит светлое завтра, и у нас появятся aspect-ratio и умная функция attr(). Но уже сейчас можно рассчитывать соотношения сторон фреймов и видеороликов, используя кастомные свойства и встроенные в CSS математических функции, они работают в большинстве современных браузеров. Для поддержки старых браузеров всё-таки придётся воспользоваться способом с паддингом или JS.

Ссылки по теме:
aspect-ratio
Designing An Aspect Ratio Unit For CSS
Setting Element Width Based on Height Via CSS
Maintain the aspect ratio of a div with CSS
Fluid Width Video
Mathematical Expressions: calc(), min(), max(), and clamp()
Attribute References: attr()
attr()
When Sass and New CSS Features Collide
Единицы размеров в CSS
Математические функции в CSS
Недоступность в картинкахМатематические функции в CSS
Наверх