Великолепный 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-логике самое простое и компактное. Осталось только дождаться хорошей поддержки.