Великолепный Range Syntax | CSS Боль
В этом курсе мы разберём несколько способов выделения диапазона дат в календаре на чистом CSS:
- на базе нового селектора
:nth-child(of S); - «фолбэчный» реактивный CSS на
clamp(); - реактивный CSS на Range Syntax в стилевых запросах.
Видеоверсия этого курса доступна во ВКонтакте и на Ютюбе
Календари удобно размечать с помощью таблиц. Ряды — это недели, а ячейки — дни.
При такой разметке для стилизации календарей отлично подходят селекторы семейства :nth-child. Например, можно использовать :nth-child(6) и :nth-child(7), чтобы оформить выходные дни.
Выделить конкретную дату с помощью :nth-child уже сложнее. Нужно подбирать значения для пересечения строки и столбца. Для каждого дня получается сложный селектор:
tr:nth-child(X) td:nth-child(Y)
Если нужно выделить диапазон дней, количество селекторов резко увеличивается. Сколько дней нужно выделить — столько и потребуется селекторов.
Представьте, сколько селекторов потребуется для выделения полного месяца.
И это не всё.
Подход с :nth-child очень хрупкий. Когда вы изменяете месяц, диапазон выделенных дат смещается. Если нужно сохранить тот же диапазон выделенных дат при изменении месяца, придётся переписывать все селекторы.
В общем, обычный :nth-child — плохой способ для выделения диапазона дат.
Возможно, обновлённый селектор :nth-child(… of S) лучше справится с задачей.
В отличие от обычного :nth-child(), обновлённый :nth-child(… of S) позволяет фильтровать элементы перед выбором по порядковому номеру.
Что если сначала выбрать все ячейки .day-now, а затем выбрать четвёртую из них с помощью :nth-child(4 of .day-now)?
Сработало, но не так, как ожидалось.
Селектор выбрал не четвёртую ячейку из всех ячеек с классом .day-now.
Он выбрал четвёртую ячейку с классом .day-nowв каждой строке таблицы.
Если бы :nth-child(… of S) работал в два прохода.
На первом проходе он собрал бы промежуточный список элементов по селектору S из всего DOM-дерева.
И на втором проходе он позволял бы выбрать элемент по порядковому номеру в промежуточном списке.
Тогда задача выбора дня решилась бы легко.
Но :nth-child(… of S) работает не так. Элементы, среди которых идёт выборка по порядковому номеру, должны быть соседними. Они должны находиться внутри одного родителя.
Поэтому :nth-child(4 of .day-now) выбирает четвёртую ячейку с классом day-now в каждом ряду таблицы.
Таким образом, нужно искать другой подход для решения задачи.
Попробуем применить «реактивный CSS» — подход, который использует условную логику прямо в CSS. В этом подходе мы сравниваем CSS-переменные и, в зависимости от результата, применяем к элементам разные стили.
Добавим главному тегу компонента (диву с классом calendar) две переменные: --day-start и --day-end, которые обозначают начало и конец диапазона дат.
Для каждой ячейки .day-now, которая обозначает день текущего месяца, добавим переменную --day, содержащую номер дня. Логичнее всего добавить эти переменные в разметке с помощью инлайновых стилей.
Начнём писать условную логику. Сначала реализуем «фолбэчный» алгоритмический подход. Он работает в большинстве браузеров, так как здесь не используются новые экспериментальные функции CSS.
Для отображения значений CSS-переменных используем экспериментальную CSS-библиотеку preview.css. Выведем значения переменной --day для каждой ячейки. Если библиотека работает, вы увидите значения в правом нижнем углу.
К каждой ячейке добавим переменную --gte-start. Вычтем из номера текущего дня номер стартового дня диапазона и сохраним результат в этой переменной.
Нулевое значение переменной --gte-start получится для десятого дня. Во всех днях до десятого значения будут отрицательными, а в последующих днях — положительными.
Теперь обернём выражение в функцию clamp(), которая позволяет «обрезать» минимальное и максимальное значения выражения.
Минимальное значение установим на 0, а максимальное — на 1. Похоже на бинарную логику, не правда ли? У всех ячеек, где дата больше 10, значение --gte-start равно единице, а в остальных оно равно нулю.
Чтобы десятый день отображался как единица, а не ноль, добавим единицу к выражению внутри clamp().
В результате у нас получится набор ячеек, номер дня которых больше или равен номеру стартового дня диапазона.
Выполним аналогичные действия, чтобы найти ячейки, которые меньше конечного дня диапазона.
Введём ещё одну переменную --lte-end. Вычтем из --day-end значение --day и сохраним результат в новую переменную. Теперь отрицательные значения --lte-end находятся справа от 12, а положительные — слева.
Ограничим значение выражения с помощью clamp() от нуля до единицы.
И включим последний день в диапазон, добавив единицу к выражению внутри clamp().
Чтобы получить ячейки, которые попадают в пересечение диапазонов «больше или равно дате начала» и «меньше или равно дате конца», нужно перемножить значения --gte-start и --lte-end.
Сохраним результат умножения в переменную --in-range. Это аналог булевой переменной-флага в обычных языках программирования. Если значение флага равно единице, то ячейка находится внутри заданного диапазона.
Наш диапазон — с 10 по 12 включительно. Ячейки 10, 11 и 12 содержат флаг с единицей. Всё работает.
Изменим стартовую дату диапазона. Флаги изменились правильно.
В чём польза бинарных флагов? Их удобно использовать для простой стилизации. Например, они могут задать полную прозрачность или непрозрачность в rgba.
Добавим ячейкам фоновое изображение — одноцветный градиент. Включим в него --in-range. Ячейки, у которых --in-range содержит единицу, получат красный фон.
«Фолбэчное» решение готово. Оно правильно реагирует на смену месяца. Мы изменили декабрь на ноябрь, и нужные ячейки оказались выделены.
Да, это решение сложное. Оно требует множества бинарных вычислений и творческого подхода к работе с нулями и единицами при стилизации. Зато у него отличная поддержка в браузерах, так как clamp() «Widely available» с июля 2020 года.
Теперь реализуем современный подход на основе новинки 2025 года — «range syntax for style queries».
В этом подходе не требуются промежуточные переменные и сложные вычисления. Просто используем CSS-функцию if() и сравниваем переменные, как в обычном языке программирования.
if(
style(--day-start < --day): ...;
else: ...;
);
Внутри условного выражения style(...) можно использовать двойной диапазон:
style(--day-start < --day < --day-end)
Так мы выполняем нужную проверку в одной строке кода.
Можно использовать операторы <= и >=.
Проверка на вхождение в диапазон, включающая границы диапазона, получается такой:
style(--day-start <= --day <= --day-end)
Современный CSS удивителен.
Проверка корректно работает, даже если начало и конец диапазона совпадают.
Вернём наш исходный диапазон с 4 по 6 число включительно.
И убедимся, что всё корректно работает при смене месяца.
Решение с использованием современной CSS-логики самое простое и компактное. Осталось только дождаться хорошей поддержки браузерами.