Содержание
  1. Почему в HTML нет цикла и чем его заменяют
  2. Что такое препроцессоры CSS и что генерируют их циклы
  3. Циклы в Sass: директивы @for, @each и @while
  4. Циклы в Less: рекурсивные миксины, each() и range()
  5. Сетка на 12 колонок: прогон примера и две ошибки
  6. Анимация циклом: пример, который не компилируется
  7. Преимущества и недостатки циклов в препроцессорах
  8. Частые вопросы про циклы в HTML и препроцессорах
ГайдыОбновлено · 09.2026

Как писать циклы с помощью препроцессоров CSS

6 июня 2024 · 12 минут чтения

Цикла в HTML нет: разметка описывает структуру страницы и повторять блоки сама не умеет. Повтор берут на себя три инструмента — шаблонизатор на сервере, JavaScript в браузере и препроцессор CSS, когда размножить нужно стили: колонки сетки, классы цветов, шаги анимации. Циклы в препроцессорах Sass и Less разбираем ниже на коде, который прогнали через компилятор.

  • Повторяющуюся разметку генерирует шаблонизатор: в Django это тег {% for %}, в Jinja и Pug — свои аналоги.
  • В браузере ту же работу делает JavaScript: массив данных плюс элемент template из спецификации HTML.
  • Однотипные CSS-классы удобнее генерировать циклом препроцессора: @for, @each, @while в Sass; рекурсивные миксины и each() в Less.
  • Два примера из прежней версии этой статьи компилятор 2026 года отвергает — разбор ошибок и рабочие замены ниже.

Почему в HTML нет цикла и чем его заменяют

Три маршрута повтора: что повторяется и где это выполняется
Три маршрута повтора: что повторяется и где это выполняется

HTML — язык разметки: его словарь состоит из элементов, у которых нет ни счётчика, ни условия повторения. Ближайшее к циклу, что даёт сама спецификация, — элемент template. Стандарт HTML Living Standard описывает его так: «The template element is used to declare fragments of HTML that can be cloned and inserted in the document by script» — фрагмент объявляется в разметке, а клонирует и вставляет его скрипт.

Отсюда три рабочих маршрута. Первый — шаблонизатор на сервере: документация Django про тег for говорит буквально «Loops over each item in an array, making the item available in a context variable». Второй — JavaScript в браузере: массив, обход, вставка клона template. Третий — препроцессор CSS: он повторяет правила стилей, и именно за этим в вёрстке чаще всего идут, когда ищут «цикл в HTML».

Маршрут Что повторяет Где выполняется Типовая задача
Шаблонизатор (Django, Jinja, Pug) Разметку по данным Сервер, до отдачи страницы Список товаров, таблица заказов, письмо рассылки
JavaScript Разметку по массиву в памяти Браузер, после загрузки Динамический список, подгрузка карточек
Препроцессор CSS (Sass, Less) Правила стилей Сборка проекта, до публикации Сетка колонок, классы цветов, шаги @keyframes

Правило выбора простое. Меняются данные — берите шаблонизатор или JavaScript. Меняется число однотипных классов, а данные при этом известны заранее — берите цикл препроцессора: он отработает один раз при сборке, и в браузер уедет обычный CSS без единой лишней инструкции. Про нумерацию итераций и то, почему счётчик почти везде стартует с нуля, у нас есть отдельный разбор; серверный маршрут с циклом по данным подробно разобран в материале про шаблонизацию с Jinja.

Что такое препроцессоры CSS и что генерируют их циклы

Препроцессоры CSS — это инструменты, которые расширяют обычный CSS переменными, вложенными правилами, миксинами и циклами. Исходник пишется на языке препроцессора, компилятор превращает его в обычный CSS, и уже этот файл подключает HTML-страница. Sass выпускается в двух синтаксисах (SCSS и отступный Sass), Less работает поверх JavaScript-компилятора.

Цикл в препроцессоре решает одну задачу: сгенерировать семейство однотипных правил из одного описания. Сетка на 12 колонок, десять модификаторов отступа, набор классов под палитру, кадры анимации — всё это пишется пятью строками исходника вместо сотни строк ручного CSS. Дальше — конкретные директивы обоих препроцессоров.

Циклы в Sass: директивы @for, @each и @while

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

@for — цикл по счётчику

Документация Sass описывает директиву дословно так: «The @for rule, written @for <variable> from <expression> to <expression> { … } or @for <variable> from <expression> through <expression> { … }, counts up or down from one number (the result of the first expression) to another (the result of the second) and evaluates a block for each number in between». Разница между двумя формами там же: «If to is used, the final number is excluded; if through is used, it’s included». То есть запись from 1 through 10 даёт десять итераций, запись from 1 to 10 — девять.

@for $i from 1 through 10 {
  .item-#{$i} {
    width: 100px * $i;
  }
}

Конструкция #{$i} — интерполяция: значение переменной подставляется прямо в имя селектора. На выходе получаются классы .item-1 … .item-10 с шириной от 100px до 1000px.

@each — цикл по списку и по карте

Директива @each перебирает готовый набор значений. Официальная справка формулирует назначение так: «The @each rule makes it easy to emit styles or evaluate code for each element of a list or each pair in a map». Ключевая деталь, которой не было в прежней версии этой статьи: имена цветов в списке нужно брать в кавычки. На списке из голых red, green, blue Dart Sass 1.104.1 печатает предупреждение «You probably don’t mean to use the color value red in interpolation here» и советует «Always quote color names when using them as strings or map keys». Рабочий вариант — карта «имя: значение»:

$palette: ("brand": #16a34a, "accent": #7c3aed);

@each $name, $color in $palette {
  .text-#{$name} {
    color: $color;
  }
}

Такой цикл выдаёт .text-brand и .text-accent с нужными цветами. Карта заодно решает задачу словаря: добавили пару — получили класс, трогать цикл не пришлось.

@while — цикл по условию

Третья директива повторяет блок, пока выражение истинно. Справка Sass: «The @while rule, written @while <expression> { … }, evaluates its block if its expression returns true. Then, if its expression still returns true, it evaluates its block again. This continues until the expression finally returns false». Счётчик здесь меняет сам разработчик — забытая строка увеличения даст бесконечный цикл и зависшую сборку.

$i: 1;

@while $i < 6 {
  .step-#{$i} {
    margin-left: 10px * $i;
  }
  $i: $i + 1;
}

Пять классов .step-1 … .step-5 с отступом от 10px до 50px. Сама документация Sass советует держать @while про запас и предупреждает: «Although @while is necessary for a few particularly complex stylesheets, you’re usually better of using either @each or @for if either of them will work. They’re clearer for the reader, and often faster to compile as well». Практический вывод: пока задача описывается счётчиком или списком, @while брать незачем.

Мини-вывод по Sass: @for отвечает за диапазон чисел, @each — за готовый набор значений, @while остаётся для расчётных условий: уменьшать кегль, пока он больше базового.

Циклы в Less: рекурсивные миксины, each() и range()

В Less отдельной директивы цикла нет, и документация предлагает вместо неё рекурсию. Формулировка официальной справки: «In Less a mixin can call itself. Such recursive mixins, when combined with Guard Expressions and Pattern Matching, can be used to create various iterative/loop structures». Миксин вызывает сам себя, а условие выхода живёт в охраннике when.

.loop(@i) when (@i > 0) {
  .item-@{i} {
    height: (50px * @i);
  }
  .loop(@i - 1);
}

.loop(5);

Классы .item-5 … .item-1 выводятся от большего к меньшему: рекурсия идёт на убывание счётчика. Интерполяция в Less пишется фигурными скобками после собаки — @{i}, и это единственная форма, которую принимает парсер селекторов.

each() и range() — современная замена рекурсии в Less

Рекурсивный миксин остаётся рабочим приёмом, но с версии 3.7.0 у Less есть функция each(), и код с ней читается ближе к @each из Sass. Официальное описание функции короткое: «Bind the evaluation of a ruleset to each member of a list». Параметры там же: «list — a comma or space separated list of values», «rules — An anonymous ruleset/mixin». Внутри блока доступны переменные @value, @key и @index, причём справка уточняет: «For most lists, @key and @index will be assigned the same value (numerical position, 1-based)».

@colors: red, green, blue;

each(@colors, {
  .@{value}-text {
    color: @value;
  }
});

Результат — три класса .red-text, .green-text, .blue-text. Счётчиковый цикл собирается из пары функций: справка Less описывает приём как «You can emulate a for loop simply by generating a numerical list and using each to expand it to a ruleset» и помечает его «Requires Less v3.9.0».

each(range(4), {
  .col-@{value} {
    height: (@value * 50px);
  }
});

На выходе .col-1 … .col-4 с высотой 50px, 100px, 150px, 200px. Обе конструкции мы прогнали через компилятор lessc 4.9.1: они собрались без предупреждений и дали ровно те классы, что описаны в справке.

Мини-вывод по Less: под список берите each(), под диапазон — each() с range(), рекурсивный миксин оставьте для случаев, где на каждом шаге нужна дополнительная логика в охраннике when.

Сетка на 12 колонок: прогон примера и две ошибки

Путь цикла препроцессора от исходника до страницы
Путь цикла препроцессора от исходника до страницы

Самое частое применение цикла в вёрстке — сетка. Прежняя версия этой статьи предлагала для неё такой код, и мы отправили его в компилятор Dart Sass 1.104.1 (строка версии: 1.104.1 compiled with dart2js 3.13.3):

$columns: 12;
$gutter: 10px;

@for $i from 1 through $columns {
  .col-#{$i} {
    width: (100% / $columns) * $i - $gutter;
    margin-right: $gutter;
  }
}

CSS-файл не собрался. Компилятор выдал предупреждение и следом ошибку:

DEPRECATION WARNING [slash-div]: Using / for division outside of calc() is deprecated
and will be removed in Dart Sass 2.0.0.

Recommendation: math.div(100%, $columns) or calc(100% / $columns)

Error: 8.333333333333334% and 10px have incompatible units.

Ошибки две, и обе стоит понимать отдельно. Первая — деление слэшем. Страница Sass про это ломающее изменение объясняет причину: «Because Sass is a CSS superset, we’re matching CSS’s syntax by redefining / to be only a separator. / will be treated as a new type of list separator, similar to how , works today. Division will instead be written using the new math.div() function». Вторая ошибка о другом: проценты и пиксели Sass вычесть друг из друга не может — смешанные единицы считает браузер внутри CSS-функции calc(). Рабочая версия выглядит так:

@use "sass:math";

$columns: 12;
$gutter: 10px;

@for $i from 1 through $columns {
  .col-#{$i} {
    width: calc(#{math.div(100%, $columns) * $i} - #{$gutter});
    margin-right: $gutter;
  }
}

Наш прогон этих 11 строк дал 59 строк CSS весом 822 байта: двенадцать классов от .col-1 до .col-12, первый с шириной calc(8.3333333333% — 10px), последний — calc(100% — 10px). Исходник вырос в готовый CSS в 5,4 раза по строкам. Тот же файл с $columns: 24 дал 119 строк и 24 класса — правил вдвое больше, строк в Sass столько же.

Анимация циклом: пример, который не компилируется

Второй пример из прежней версии статьи обещал собрать кадры @keyframes рекурсивным миксином Less. Мы прогнали и его — через lessc 4.9.1:

@keyframes anim {
  .loop(@i) when (@i > 0) {
    @{50% / 5 * @i} {
      transform: translateY(@i * 10px);
    }
    .loop(@i - 1);
  }
  .loop(5);
}

Компилятор остановился на третьей строке:

ParseError: Unrecognised input in old_kf.less on line 3, column 11:
2   .loop(@i) when (@i > 0) {
3     @{50% / 5 * @i} {
4       transform: translateY(@i * 10px);

Причина в интерполяции. Между фигурными скобками Less ждёт имя переменной, выражение он там не разбирает. Значит арифметику надо вынести в переменную, а в селектор подставить уже её:

@keyframes anim {
  .kf(@i) when (@i =< 5) {
    @p: (@i * 20%);
    @{p} {
      transform: translateY(@i * 10px);
    }
    .kf(@i + 1);
  }
  .kf(1);
}

Этот вариант собрался и дал пять кадров — 20%, 40%, 60%, 80% и 100% со сдвигом от 10px до 50px. Порядок кадров идёт по возрастанию, потому что счётчик растёт: охранник when (@i =< 5) держит верхнюю границу, а вызов .kf(@i + 1) двигает шаг.

Преимущества и недостатки циклов в препроцессорах

Цикл экономит строки исходника и держит однотипные правила в одном месте: поменяли шаг сетки — пересобрали весь набор классов. Обратная сторона видна на тех же примерах выше. Отладка идёт по сгенерированному CSS, где номера строк уже не совпадают с исходником. Ошибка в одной строке цикла размножается на все итерации. Большой цикл с вложенными миксинами заметно удлиняет сборку, а читателю чужого проекта труднее понять, откуда взялся класс, которого нет в поиске по коду.

Ситуация Решение
Нужен диапазон чисел (сетка, отступы, z-index) Sass: @for from … through; Less: each(range(N), {…})
Нужен обход готового набора (палитра, иконки, брейкпоинты) Sass: @each по карте; Less: each(@list, {…})
Значение считается на каждом шаге до порога Sass: @while; Less: рекурсивный миксин с when
Повторяется сама разметка по данным Шаблонизатор на сервере или JavaScript в браузере
Деление в Sass math.div() после @use «sass:math» либо calc()

Отдельный класс задач — компонентные фреймворки, где цикл живёт прямо в шаблоне: во Vue за повтор узла отвечает директива v-for, и её разбирают на собеседованиях наравне с реактивностью — см. наши практические вопросы по Vue.js.

Частые вопросы про циклы в HTML и препроцессорах

Можно ли написать цикл прямо в HTML? Нет. HTML описывает структуру документа и не содержит оператора повторения. Повтор разметки делают шаблонизатор на сервере (например, тег {% for %} в Django) или JavaScript в браузере, а повтор однотипных стилей — циклы препроцессоров Sass и Less.

Чем @for отличается от @each в Sass? @for идёт по диапазону чисел и подставляет счётчик, @each перебирает готовый список или карту и подставляет значение. Форма through включает последнее число диапазона, форма to его исключает.

Есть ли в Less директива цикла? Отдельной директивы нет. Документация Less предлагает рекурсивные миксины с охранником when, а с версии 3.7.0 доступна функция each(); счётчиковый цикл собирается связкой each(range(N), {…}), которая требует Less 3.9.0.

Почему пример с делением через слэш перестал работать? В Dart Sass деление слэшем объявлено устаревшим и будет удалено в версии 2.0.0, вместо него используется функция math.div() из модуля sass:math. Компилятор 1.104.1 печатает об этом предупреждение slash-div.

Как избежать бесконечного цикла в @while? Менять переменную условия внутри тела цикла и держать проверку простой. Когда задача сводится к диапазону или списку, документация Sass рекомендует брать @for или @each — они понятнее читателю и быстрее компилируются.

Материал обновлён 16 сентября 2026 года. Весь код проверен прогоном на Dart Sass 1.104.1 и lessc 4.9.1.

Читайте также

3 материала