Реактивный CSS
Реактивный CSS — это новый подход к вёрстке, при котором весь внешний вид компонента зависит от CSS-переменных состояния. В этом подходе привычная логика отображения «переезжает» из JavaScript непосредственно в CSS.
Подход работает в Chrome и опирается на нативные возможности CSS для работы с условиями: стилевые контейнерные запросы и операции с переменными.
Измените значение --current на 2, 3 или 4 и убедитесь, что слайдер переключается.
Видеоверсия этого курса доступна во ВКонтакте и на Ютюбе
В этой демонстрации мы создадим две реализации слайдера — традиционную, с жонглированием дополнительными классами в JS, и в подходе «реактивного CSS». Затем сравним их, чтобы показать:
- как логика из JS перетекает в стили;
- каковы объём и сложность реализации этой логики на CSS.
Вы сможете сами оценить, является ли это «сборником костылей» или решением, имеющим право на будущее. Начнём с разметки списка слайдов.
Крупными штрихами соберём базовую стилизацию слайдера. Зададим фиксированные размеры и относительное позиционирование контейнеру слайдов.
С помощью абсолютного позиционирования привяжем .slide-image к контейнеру по положению и размерам. Разместим элементы друг над другом.
Стилизуем сами изображения.
Добавим в разметку список переключателей.
Важно, чтобы в списке слайдов и в списке переключателей было одинаковое количество элементов, равное количеству слайдов — по четыре.
Кнопки переключения вперёд и назад находятся внутри первого и последнего элементов списка, рядом с кнопками 1 и 4.
Выстроим управляющие элементы в ряд с помощью флексбокса.
Стилизуем кнопки.
Добавим стили для интерактивных состояний.
На этом этапе базовая структура стилей и разметки завершена.
Вынесем стили в отдельный файл slider-base.css и больше не будем к ним возвращаться. Эти стили будут использоваться без изменений в обеих реализациях слайдера.
Добавим стили для переключения слайдов.
Класс .slide-item--current используется для отображения текущего изображения.
Класс .control-btn--current подсвечивает кнопку, которая соответствует текущему слайду.
Кнопки «Вперёд» и «Назад» деактивируются с помощью атрибута disabled.
Дополнительное оформление для отключённого состояния зададим с помощью .control-btn-prev[disabled] и .control-btn-next[disabled].
Чтобы переключить слайд, нужно переместить класс slide-item--current на соответствующий элемент списка слайдов, а класс control-btn--current — на соответствующую кнопку. Также необходимо добавить или удалить атрибут disabled на кнопках «Вперёд» и «Назад».
Подключим script.js и напишем функцию, которая реализует логику переключения слайдов традиционным способом.
Функция создана с помощью ИИ, которому была поставлена задача написать простую функцию, как это сделал бы мидл фронтенд-разработчик на чистом JS.
Сначала найдём все DOM-элементы, с которыми будем взаимодействовать. Также зададим начальное состояние слайдера: let current = 1;.
Создадим функцию для переключения слайда, внутри которой будет обрабатываться бизнес-логика работы компонента. Сначала проверим, что входное значение находится в допустимом диапазоне индексов. Затем обновим состояние компонента, изменив номер текущего слайда:
current = newIndex;
Добавим обработчики событий для кнопок, которые будут вызывать функцию переключения слайда.
Добавим вызов пока ещё пустой функции updateSlider, которая обновляет внешний вид слайдера.
Если в switchSlider содержится бизнес-логика, которая может изменять состояние слайдера, то в updateSlider находится только логика отображения. Логика отображения может только отражать состояние компонента, но не менять его.
А дальше начинается «любимое» всеми жонглирование классами, которые мы подготовили заранее.
Чтобы отобразить нужный слайд, сначала удаляем класс slide-item--current у всех элементов. Затем по индексу добавляем этот класс к текущему элементу.
Аналогичную операцию выполним для кнопок с номерами слайдов и классом control-btn--current.
Отдельно проверим состояние кнопок «Вперёд» и «Назад».
Напоследок вызовем функцию updateSlider с номером текущего элемента, чтобы синхронизировать состояние функции с исходной разметкой.
Традиционная реализация готова. Она представлена в упрощённом виде, без сложных оптимизаций, чтобы её было проще сравнить с подходом «реактивный CSS».
Теперь реализуем новый подход. Удалим скрипты и стили для состояний. Перейдём к разметке. В ней найдём дополнительные классы состояний и атрибуты.
Удалим их, так как в новом подходе для стилизации они не понадобятся.
Добавим к корневому тегу компонента .slider CSS-переменную состояния --current, которая будет задавать номер текущего слайда, и присвоим ей значение 1.
Перейдём к стилям. Напомним, что мы используем те же базовые стили, что и в первой реализации. Изменяются только стили, которые отвечают за отображение состояний элементов слайдера.
Начнём со стилизации активного слайда. Сначала зарегистрируем промежуточную переменную --is-current-image с помощью директивы @property.
Затем для элементов списка (.slide-item) вычислим значение промежуточной переменной с помощью выражения:
sign(sibling-index() - var(--current))
Это выражение сравнивает номер текущего слайда из переменной --current с индексом каждого элемента списка. Если индекс элемента равен номеру слайда, то результатом будет 0.
Добавим стилевой контейнерный запрос, чтобы переопределить стили элементов, которые находятся внутри элементов с переменной --is-current-image, равной 0.
Переопределим значение z-index для элемента .slide-image.
Элементы .slide-image находятся внутри элементов списка .slide-item, где мы вычисляли промежуточную переменную.
Поскольку изображение изменилось, контейнерный запрос сработал.
Перейдём к разметке и протестируем стили. Будем изменять значение --current.
Сейчас отображается изображение, которое находится внутри первого .slide-item. Это логично, так как значение --current равно единице.
Изменим значение переменной --current на 2. Теперь отображается изображение из второго элемента .slide-item.
Это значит, что стили работают правильно!
Выполним аналогичные действия для стилизации кнопки с текущим номером слайда.
Снова создадим вспомогательную переменную --is-current-control.
Вычисляем её значение по такой же формуле:
--is-current-control: sign(sibling-index() - var(--current))
Важно. Чтобы получить правильные индексы, необходимо вызывать sibling-index() на элементах списка и «замораживать» полученное значение.
Значение зарегистрированной переменной вычисляется сразу при её объявлении, что позволяет нам «заморозить» значение.
Добавим ещё один стилевой контейнерный запрос, чтобы переопределить стили активной кнопки (той, которая находится внутри элемента с --is-current-control: 0).
Правильная кнопка подсветилась. Попробуйте удалить @property и убедитесь, что без него стили не работают.
Теперь займёмся кнопками «Назад» и «Вперёд». Для них нам понадобятся «глобальные» флаги, которые показывают, является ли активным первый или последний элемент.
Да, можно обойтись без флагов, мы введём их для единообразия и читабельности. Зарегистрируем первый флаг --is-first-control.
Внутри элементов .control-item сравним значение --current с единицей и запишем результат в --is-first-control.
Теперь во всех.control-item значение --is-first-control будет одинаковым, так как --current одинаково для всех. Поэтому этот флаг называется «глобальным».
Стилизуем кнопку .control-btn-prev, когда --is-first-control равен нулю (то есть активен первый элемент слайдера).
Значение флага одинаково для всех элементов списка, но класс кнопки «Назад» уникален в пределах списка. Поэтому стилизация работает правильно: когда активен первый слайд, изменяется только одна кнопка.
Переключимся на последний слайд. Кнопка «Назад» снова активна. Стили работают.
По аналогии добавим второй флаг, который покажет, активен ли последний слайд.
Зарегистрируем переменную --is-last-control.
Сравним значение --current с количеством слайдов (элементов списка), которое получаем с помощью sibling-count(), и результат запишем в --is-last-control.
Значение переменной также будет одинаковым для всех элементов списка, так как sibling-count() одинаков для всех них.
Переопределим стили .control-btn-next, когда активен последний слайд слайдера.
На этом логика отображения слайдера завершена. Она полностью реализована на уровне стилей. Теперь вы можете вручную изменять значение --current на корневом элементе компонента и тестировать переключение слайдов.
Следующий шаг — подключить скрипт для переключения слайдов и обновить его с учётом новой стилизации.
Возьмём за основу старый скрипт переключения слайдов и удалим из него код внутри функции updateSlider. Всё остальное оставляем без изменений.
То есть, бизнес-логику, которая изменяет состояние компонента, обрабатывает события и так далее, мы не трогаем. Она и должна оставаться на стороне JavaScript. А вот логику отображения мы полностью изменяем.
Реализуем новую функцию для обновления отображения слайдера. Вместо всех циклов и условий, которые управляли классами и атрибутами, нам теперь понадобится всего одна строка кода, которая изменяет CSS-переменную --current:
slider.style.setProperty('--current', current);
Скрипт готов. Можно тестировать переключение слайдов.
Во вкладках style-old.css и script-old.js находятся старые версии скриптов и стилей. Давайте сравним их с новой реализацией. Например, вот как выглядело переключение картинки в JavaScript:
slides.forEach(function(slide) {
slide.classList.remove('slide-item--current');
});
slides[current - 1].classList.add('slide-item--current');
А в CSS эта же логика теперь выглядит так:
.slide-item {
--is-current-image: sign(sibling-index() - var(--current));
}
@container style(--is-current-image: 0) {
...
}
Давайте ещё немного поэкспериментируем со стилизацией новой версии слайдера. Посмотрите ещё раз на выражение:
--is-current-image: sign(sibling-index() - var(--current))
Оно возвращает -1 для всех элементов, которые находятся перед текущим, 0 для текущего элемента и 1 для тех, которые находятся после. Это значит, что мы можем сместить все картинки до текущей влево с помощью стилевого запроса style(--is-current-image: -1).
Затем мы можем сместить все картинки, которые находятся после текущей, вправо с помощью запроса style(--is-current-image: 1).
Всего двумя строчками кода мы изменили внешний вид слайдера, и он превратился в «стопку» слайдов.
Можно сделать ещё более красивую «стопку», в которой видны все слайды. Для этого нужно определить «расстояние» от каждого слайда до текущего. Зарегистрируем переменную --current-distance.
Затем вычислим значение этой переменной с помощью выражения:
--current-distance: abs(sibling-index() - var(--current))
Функция abs() возвращает абсолютное значение выражения. Таким образом, если текущий слайд — 3, то слайды с номерами 1 и 5 будут находиться на расстоянии 2 от текущего слайда.
Теперь мы можем заменить статичные значения свойств на формулы, которые используют расстояние до текущего слайда.
Например, для свойства left можно подобрать формулу, которая смещает картинки в зависимости от расстояния. Чем больше расстояние, тем больше смещение.
Аналогично, можно подобрать формулу для уменьшения масштаба. Чем больше расстояние, тем меньше значение в scale(), и, соответственно, меньше размер картинки.
Переключитесь на 4-й слайд и посмотрите, как выглядит стопка картинок слева от текущего.
Также можно задать формулы для слайдов, расположенных после текущего. Используем те же формулы, но смещение происходит с помощью свойства right, а не left.
Переключитесь на 1-й слайд и посмотрите на стопку справа. С размерами и смещениями всё в порядке, но порядок перекрытия выглядит странно.
Порядок перекрытия можно исправить, если заменить статичное значение z-index для .slide-image на динамическое. Формула простая:
calc(-1 * var(--current-distance))
Таким образом, чем дальше слайд от текущего, тем ниже он находится в стеке.
Переключимся на первый слайд и убедимся, что стопка картинок справа отображается корректно.
С помощью небольшой доработки стилей мы усложнили внешний вид элемента. При этом никаких изменений в JavaScript не потребовалось — мы по-прежнему изменяем только значение --current при переключении слайдов.
Теперь представьте, сколько кода пришлось бы добавить в традиционную реализацию слайдера (и в скрипты, и в стили), чтобы добиться такого же поведения.
При желании можно реализовать анимацию переключения слайдов прямо в CSS.
Да, текущая стилизация не предназначена для создания красивой анимации. Мы использовали простейшее решение с transition: all. Однако, даже это решение подходит для простых случаев.
Благодаря использованию sibling-функций, реактивная стилизация отлично работает при изменении количества слайдов. Добавим ещё один слайд и убедимся, что ничего не сломалось. Важно следить за тем, чтобы количество элементов в списках с картинками и кнопками совпадало.
Вот такой он — реактивный CSS. Пока неясно, какая судьба ждёт этот подход, но одно очевидно — коробка Пандоры открыта.