Помните сказку про мальчика, который кричал «волки»? Примерно так же в 2025 году случилось с «программированием на CSS». Вышла функция if(). Блогеры преждевременно хайпанули: всё, теперь у нас условия в CSS. Разработчики пошли читать спецификации, попробовали — и довольно быстро выяснилось, что внутри условного выражения style() возможностей почти нет. Многие разочаровались и похоронили идею.

В конце 2025 года Chrome выкатил революционный Range Syntax For Style Container Queries. Обновлённый style() научился сравнивать переменные между собой и поддерживать диапазонные выражения. Мы наконец‑то получили мощную условную логику в CSS, но мало кто это заметил.

В этой статье мы попытаемся реанимировать идею программирования на CSS. На примере интерфейсного паттерна — «выделение диапазона дат в календаре» — разберём, как обычная JS‑логика превращается в CSS‑логику (спойлер: очень просто).

Заинтригованы? Поехали.

Задача: выделение диапазона дат

Выделение диапазона дат в календаре
Выделение диапазона дат в календаре

Диапазоны дат встречаются повсюду:

  • бронирование отеля,

  • выбор отпуска,

  • спринты в планировщике,

  • фильтр по периоду в аналитике.

Типовая формулировка задачи звучит так: подсветить все дни между стартом и концом диапазона. Есть ряд особенностей:

  • Календарь обычно делают таблицей, а не плоским списком, то есть ячейки дней расположены внутри рядов недель.

  • При смене месяца выделенный диапазон должен сохраняться.

  • Диапазон может состоять из одного дня.

  • Границы диапазона включают даты старта и конца диапазона.

Обычный подход на JS

Типичный подход — пройтись по ячейкам, проверить на попадание в диапазон и поставить нужный класс. Примерно так:

const start = 4;
const end = 6;

document.querySelectorAll('.day-now').forEach((cell) => {
  const day = Number(cell.dataset.day);
  cell.classList.toggle('in-range', day >= start && day <= end);
});

Всё работает. Проблема в том, что это исключительно «визуальная» задача. Точно такая же, как создание полосатых таблиц.

Сейчас никому и в голову не придёт делать полосатые таблицы на JS. А чем выделение диапазона дат хуже? Только тем, что у нас нет простого способа реализации на CSS. То есть не было.

Сделаем же это на CSS!


Реализация на CSS

Шаг 1: Разметка

Разметка практически не меняется. Это всё та же таблица, где ячейки — это дни, а ряды — это недели. Каждой ячейке в разметке нужно добавить CSS‑переменную с номером дня. Проще всего сделать это с помощью атрибута style:

<tr>
  <td class="day day-now" style="--day: 21">21</td>
  <td class="day day-now" style="--day: 22">22</td>
  ...
</tr>

Да, приходится специально инлайнить стили, но это делается один раз при генерации шаблона.

Шаг 2: Переменные диапазона

Добавляем корневому элементу календаря CSS‑переменные со стартом и концом диапазона. Например, так:

.calendar {
  --day-start: 4;
  --day-end: 6;
}

Шаг 3: Условная стилизация отдельных ячеек

Теперь самое интересное. Современный CSS позволяет писать условные выражения через if(). Проверка условий происходит внутри специальной конструкции style(), в соответствии с так называемым Range Syntax For Style Container Queries.

Благодаря Range Syntax можем сравнивать CSS‑переменные друг с другом, или CSS‑переменную с каким‑то значением. Причем мы можем использовать привычные операторы сравнения >, < или =.

У каждой ячейки с классом day-now есть CSS‑переменная --day с номером дня. Мы можем сравнить номер дня с начальным днём диапазона, который хранится в --day-start. И в зависимости от результата, задать ячейкам разный фон. Так выглядит сравнение:

.day-now {
  background-color: if(
    style(--day-start <= --day): #8b0000;
    else: rgba(255, 255, 255, 0.05);
  );
}

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

Диапазон дат с 4 включительно
Диапазон дат с 4 включительно

Шаг 4: Двойной диапазон

Мы можем использовать и двойной диапазон. То есть подсветить те ячейки, день которых одновременно больше или равен дню старта и меньше или равен дню конца:

.day-now {
  background-color: if(
    style(--day-start <= --day <= --day-end): #8b0000;
    else: rgba(255, 255, 255, 0.05);
  );
}

Результат:

Диапазон дат с 4 по 6 включительно
Диапазон дат с 4 по 6 включительно

Вот и всё. Решение полностью готово. С помощью одной строчки CSS. Осталось только протестировать, как всё работает. Давайте поменяем границы диапазона:

.calendar {
  --day-start: 2;
  --day-end: 20;
}

Всё корректно:

Другой диапазон дат, с 2 по 20 включительно
Другой диапазон дат, с 2 по 20 включительно

Сравнение без включения границ диапазона

Естественно, можно включать или исключать границы диапазона. Для этого в зависимости от задачи используем <= и >= или < и >.

.calendar {
  --day-start: 2;
  --day-end: 20;
}

.day-now {
  background-color: if(
    style(--day-start < --day < --day-end): #8b0000;
    else: rgba(255, 255, 255, 0.05);
  );
}

Результат:

Диапазон дат с 2 по 20, не включая границы
Диапазон дат с 2 по 20, не включая границы

Диапазон в один день

Проверим, как работает сравнение, если границы диапазона совпадают:

.calendar {
  --day-start: 6;
  --day-end: 6;
}

.day-now {
  background-color: if(
    style(--day-start <= --day <= --day-end): #8b0000;
    else: rgba(255, 255, 255, 0.05);
  );
}

Всё корректно:

Диапазон в один день
Диапазон в один день

Смена месяца

Снова устанавливаем диапазон с 4 по 6 число и меняем месяц.

.calendar {
  --day-start: 4;
  --day-end: 6;
}

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

Меняем ноябрь на декабрь. Диапазон с 4 по 6 число сохраняется
Меняем ноябрь на декабрь. Диапазон с 4 по 6 число сохраняется

Вот так внезапно CSS‑превратился в язык с полноценной условной логикой, на котором действительно можно «программировать». Реализация сравнений оказалась на удивление простой и знакомой, а объём кода минимальным.

Что с поддержкой?

Единственная ложка дёгтя — это поддержка. В нашем случае придётся ждать как минимум двух последовательных событий:

  1. Хорошей поддержки Style Container Queries. То есть самого механизма стилевых запросов. Здесь есть хорошие новости. Стилевые запросы добавили в интероп 2026, то есть в течение года планируют зарелизить во всех современных браузерах.

  2. Хорошей поддержки Range Syntax For Style Container Queries. То есть продвинутого синтаксиса сравнений внутри стилевых запросов.

Если вам хочется узнать, в чём ценность нового подхода к стилизации, то можете сразу переходить к заключительной части статьи.

А если вы хотите использовать условную логику в CSS прямо сейчас, то читайте следующий раздел, в котором мы разберём фолбэк и его ограничения.

Фолбэк CSS-условий

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

Для начала введём дополнительную переменную --gte-start и сохраним в неё результат вычитания номеров текущего и стартового дней:

Шаг 1. Вычитаем из номера текущего дня номер стартового

.calendar {
  --day-start: 10;
  --day-end: 12;
}

.day-now {
  --gte-start: calc(var(--day) - var(--day-start));
}

Возьмём десятое число как день старта. И выведем результат вычитания для каждой ячейки в правом нижнем углу. В днях до десятого получили отрицательный результат, в десятом дне — ноль, в днях после десятого — положительный результат:

Результат вычитания --day-start из --day в каждой ячейке при --day-start: 10
Результат вычитания --day-start из --day в каждой ячейке при --day-start: 10

Шаг 2. Ограничиваем результат через clamp()

Теперь заменим обычное вычитание через calc() вычитанием через clamp(). Функция clamp() позволяет ограничивать выражение максимальным и минимальным значением. Зададим минимальное значение 0, а максимальное 1:

.day-now {
  --gte-start: clamp(
    0,
    calc(var(--day) - var(--day-start)),
    1
  );
}

Теперь всё, что меньше 0 стало 0, всё, что больше 1 стало 1.

Значение выражения ограничено нулём снизу и единицей сверху с помощью clamp()
Значение выражения ограничено нулём снизу и единицей сверху с помощью clamp()

Шаг 3. Включаем стартовый день в диапазон

Для этого добавляем единицу к результату вычитания внутри clamp():

.day-now {
  --gte-start: clamp(
    0,
    calc(var(--day) - var(--day-start) + 1),
    1
  );
}

Теперь стартовый день тоже даёт 1. Мы получили выражение, которое даёт результат аналогичный сравнению текущий день ≥ день старта.

Включили день старта (10 число) в диапазон
Включили день старта (10 число) в диапазон

Шаг 4. Добавляем вторую границу

По аналогии с первой границей диапазона добавим вторую, включающую дни которые меньше или равны конечному дню. Единственное отличие — это порядок вычитания. Теперь мы вычитаем не границу из дня, а день из границы:

.calendar {
  --day-start: 10;
  --day-end: 12;
}

.day-now {
  --gte-start: clamp(0, calc(var(--day) - var(--day-start) + 1), 1); 
  --lte-end: clamp(0, calc(var(--day-end) - var(--day) + 1), 1);
}

Второе выражение даёт результат аналогичный сравнению текущий день ≤ день конца. Вот такие значения будут в переменной --lte-end, если конечный день — 12.

Значение --lte-end в каждой ячейке при --day-end: 12
Значение --lte-end в каждой ячейке при --day-end: 12

Шаг 5. Пересечение диапазонов

Используем умножение, чтобы получить логическое И:

  • 1 × 1 = 1,

  • в остальных случаях = 0.

.day-now {
  --gte-start: clamp( 0, calc(var(--day) - var(--day-start) + 1), 1 ); 
  --lte-end: clamp( 0, calc(var(--day-end) - var(--day) + 1), 1);
  --in-range: calc(var(--gte-start) * var(--lte-end));
}

Вот значения переменной --in-range. Единицы стоят у тех ячеек, которые попали в диапазон с 10 по 12 день включительно:

Значение флага  --in-range в каждой ячейке при --day-start: 10 и --day-end: 12
Значение флага --in-range в каждой ячейке при --day-start: 10 и --day-end: 12

Шаг 6. Зашиваем флаг в стили

Теперь у нас есть переменная, содержащая ноль в одних ячейках и единицу в других. Эти значения можно использовать в качестве компонента прозрачности цвета в фоновом изображении, которое сделано с помощ��ю одноцветного градиента:

.day-now {
  background-image: linear-gradient(
    rgba(139, 0, 0, var(--in-range)),
    rgba(139, 0, 0, var(--in-range))
  );
}

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

Выделение ячеек с помощью полной прозрачности/непрозрачности фона
Выделение ячеек с помощью полной прозрачности/непрозрачности фона

Плюсы, минусы и ограничения фолбэка

Плюс фолбэка — это поддержка. Его можно использовать прямо сейчас.

Минус фолбэка — сложность. Мало кто захочет разбираться и поддерживать эту имитацию бинарной логики. Хотя запредельной эту сложность назвать нельзя.

Второй минус — ограничения по стилизации. Можно работать только с числовыми значениями CSS‑свойств, которые можно вычислять с помощью нуля и единицы. Например, удобно использовать прозрачность, толщину рамок или обводок, размер шрифта. А вот работать с перечисляемыми или строковыми значениями свойств не получится.

В чём смысл «программирования на CSS»?

Если вкратце, то всё делается ради слабой связанности aka «Loose Coupling».

Вспомните типичную реализацию на JS. Сколько всего надо знать скрипту, чтобы выполнить задачу? Перечислим:

  • особенности устройства разметки (плоский список или сложная таблица);

  • особенности использования CSS‑классов (как помечается день текущего месяца, а как смежного);

  • как устроена стилизация дней диапазона (имя класса, или дополнительный класс‑модификатор, или прямая стилизация через атрибут style с конкретным свойством и значением).

А не слишком ли много? Это и называется сильная связность.

А теперь посмотрим на реализацию с использованием CSS‑логики. В ней диапазон дат тоже задаётся скриптом. Но у скрипта есть лаконичный и понятный интерфейс в виде двух CSS‑переменных, которые он просто меняет:

.calendar {
  --day-start: <number>;
  --day-end: <number>;
}

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

Вся логика отображения изолирована внутри CSS.

Это и есть слабая связанность.

Это и есть следование заветам отцов‑основателей веба про разделение содержания, внешнего вида и поведения.

Скрытый текст

Подписывайтесь на мой телеграм‑канал «CSS Боль». Там собраны все материалы, видеоролики, ссылки на пошаговые демки и новости чемпионатов по фронтенду