Используем random() правильно

• css

Введение

CSS совсем скоро стукнет 30 лет (смотря откуда считать), и практически к этой дате мы получим CSS-функцию для получения случайных значений random() в трёх браузерных движках:

Однако не всё так просто. Да и вообще, нужен ли этот random, если 30 лет жили без него?

Задача

Сама задача random не нова. Больше 10 лет назад у Романа Комарова (kizu) была статья про бросание кубика на чистом CSS, и, насколько я понимаю, он участвовал в написании спеки про random() в нынешнее время.

Зачем же вообще получать случайные значения? Тут может быть много вариантов:

  • Случайная раскраска элементов, которая бы была одинаковой для одинаковых значений. Это могут быть теги у элементов в списке, или заголовок Server-Timing, цвета для элементов которого хочется видеть одинаковыми при каждой загрузке.
  • Случайные стили одинаковых элементов. Хороший пример – стопки фотографий, где каждая стопка имеет по-разному повёрнутые фотографии.
  • Снежинки и звёздное небо. Постепенно становится холоднее, а совсем скоро сайты начнут наполняться снежинками – random отлично подходит для реализации!

Но я бы всё-таки отличал использование random от “неповторяющихся” элементов (например, принцип цикады). Случайные числа стоит использовать, если нужен разный результат, а принцип цикады – когда нужен одинаковый псевдо-случайный. Небольшая разница всё же есть.

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

random

random(min, max)
random(min, max, step)
random(seed, min, max)
random(seed, min, max, step)

min, max, step должны иметь один тип – число, размер или процент. И да, min и max – включительно, кто-то вспомнит математическую запись [min, max]. Базовое использование:

.block {
    width: random(50px, 150px);
}

Всё, на этом расходимся… Шутка. Самая большая проблема – когда будет новое случайное значение, а когда – старое. К слову, в примере выше элемент не будет получать новую ширину каждую секунду. Ещё один подводный камень – в поддержке. Разные значения seed поддерживаются по-разному.

Каждое случайное значение имеет “базу”. seed влияет на инициализацию этого базового значения и за его переиспользование. Два случайных числа с одинаковым базовым значением и одинаковыми остальными параметрами будут одинаковыми.

Я попробовал разбить на разные случаи использования.

Разные значения в разных элементах и свойствах

random(auto, <min>, <max>)
preview
preview
26.2

Базовое использование, без seed. Оно же значение auto:

/* прямоугольник */
.block {
    width: random(50px, 150px);
    /* тоже самое */
    width: random(auto, 50px, 150px);
 
    height: random(50px, 150px);
}

Это может приводить к неожиданным эффектам:

.block {
    width: random(50px, 150px);
}
 
.block:hover {
    width: random(50px, 150px);
}

Данная конструкция не приведёт к изменению размера при наведении мыши. Дело в том, что значение random() кешируется между вызовами, и при использовании auto значение получается одинаковым для одного и того же свойства одного и того же элемента. А значит, если random() позовут в другом CSS-правиле, но для того эе элемента и того же свойства, то получится такое же значение.

Есть нюанс: если вызовов random() в одном свойстве несколько, то при таком использовании каждый вызов получит своё значение. Это может быть полезно для разных компонентов цвета.

Значение auto соответствует element-scoped property-index-scoped (на них мы посмотрим позже).

Разные значения в каждом элементе, одинаковые в разных свойствах

.square {
    width: random(--size element-scoped, 50px, 150px);
    height: random(--size element-scoped, 50px, 150px);
}

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

В данном варианте использования все random внутри одного элемента получат одно и то же значение. --size – произвольный идентификатор, который будет “объединять” все случайные значения. Таким образом, не нужно беспокоиться, если --size встретится для другого компонента, ведь элемент будет другой, а значит, и случайное значение тоже будет другим.

Ещё один нюанс – элемент и его псевдоэлементы считаются разными элементами.

Одинаковые значения в разных элементах, разные для разных свойств

.block {
    background: hsl(random(property-scoped, 0deg, 360deg) 100% 50%);
}
 
.block::before {
    background: hsl(random(property-scoped, 0deg, 360deg) 100% 50%);
}

Есть два варианта: property-scoped и property-index-scoped. Оба будут получать одинаковые значения для одного свойства у разных элементов. Разница будет при использовании нескольких random() внутри одного свойства. В первом варианте значения всех random() внутри свойства будут иметь одинаковую базу, во втором – разные.

Значение может быть полезно для псевдоэлементов: если нужно получить случайный фон у основного элемента и сделать такой же фон у его декоративного псевдоэлемента (хвостик тултипа), то property-scoped как раз придётся к месту.

Одинаковые значения в разных элементах и свойствах

.block {
    background: hsl(random(--accent, 0deg, 360deg) 100% 50%);
}

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

Фиксированные значения

.block {
    width: random(fixed 0.5, 50px, 150px);
}

fixed полезно для тестирования – оно предоставляет фиксированное значение. Однако, если хочется автоматически получать в тестах фиксированное значение, а в продакшене – разное, то придётся выдумывать свою систему.

Другие варианты

Для seed можно указывать auto, fixed, набор ключевых свойств или идентификатор, начинающийся с --, рядом с которым тоже можно указать набор ключевых свойств.

Ключевые свойства такие:

  • element-scoped – добавляет элемент к “базе”
  • property-scoped – добавляет имя свойства
  • property-index-scoped – добавляет имя свойства и номер вызова random() внутри него

Некоторые комбинации рассмотрели выше. Я постарался собрать наиболее полезные по вариантам применения.

Остальные варианты тоже возможны, и их поведение будет согласно комбинации. Например, рассмотренный выше auto имеет element-scoped property-index-scoped, т..е случайная величина будет разной в разных элементах и в разных свойства.

Что с CSS-переменными?

Думаю, читатель мог не раз задаться вопросом: как это всё работает с CSS-переменными? Может, и не нужна вся эта сложность с ключевыми словами?

И действительно, на поверхности лежит логика (она НЕПРАВИЛЬНАЯ, так НЕ РАБОТАЕТ): если хочется одинаковое случайное значение, то просто поместим его в CSS-переменную:

.block {
    /* НЕ СРАБОТАЕТ */
    --size: random(50px, 150px); 
    width: var(--size);
    height: var(--size);
}

Думаю, по коду очевидно, что имелось ввиду: заводим переменную, получаем случайный размер, используем его. Что же пошло не так?

Дело в том, что CSS-переменные, не объявленные через @property, работают, как подстановки строк. В данном же случае строка random(...) подставляется два раза, а затем вычисляется по логике random: случайное значение для каждого свойства и элемента. Тем самым это будет прямоугольник, а не квадрат (если только не повезёт).

Что с @property? После объявления свойство становится “полноценным”, и random() начинает вычисляться уже для него:

@property --size {
    syntax: "<length> | <percentage>";
    inherits: true;
    initial-value: 0px;
}
 
.block {
    /* Теперь работает */
    --size: random(50px, 150px); 
    width: var(--size);
    height: var(--size);
}

Чуть-чуть аккуратнее

Есть два соображения.

  1. Имейте ввиду, что случайные значения могут приводить к плохим комбинациям. Например, можно получить маленький размер шрифта, выползающий за пределы текст или неконтрастное сочетание цветов.

  2. random() плохо тестируется и воспроизводится. Получив одну “хорошую” комбинацию значений, нельзя быть уверенным, что всегда всё будет хорошо.

Итого

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

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

В любом случае, random() - впечатляющее достижение современных браузеров. Каких-нибудь 10 лет назад про подобное можно было бы услышать “невозможно”, а теперь мы живём в мире style query, @scope, и многом другом. random() не теряется на их фоне.

Ждём random-item()?

Обсудить в Telegram

Почитать ещё посты

  • Анимированная выпадушка на CSS
  • Будущие новинки из CSS Navigation Matching
  • Поддержка фич, которые иначе отследить нельзя: named-feature
  • image-animation и контроль над анимацией
  • frame-sizing не решает проблемы размера ифреймов?