What loops do in FSCSS

Loops expand arrays into properties, selectors, or value fragments. They are compile-time (or runtime) expansions — the browser only sees plain CSS.

%n() / multiplexing

Same value on many property names

Auto-index []

:nth-child and per-item rules from index arrays

rpt()

Repeat a selector prefix (nested div chains)

inline()

Emit loop bodies inside values (e.g. clip-path)

Quick Test (browser runtime) :

<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.5/runtime.min.js" async></script>

Rules of thumb

  • Arrays are 1-indexed.
  • Use [] (empty brackets) to iterate; use [n] for a fixed index.
  • Build index lists with count(@arr.name!.length) or count(n, 1) (start at 1).
  • Put empty{ /* preserve */ } before loop-driven rules when intermediate tokens would otherwise become stray selectors.
  • %n(...) → property lists · rpt() + @arr[] → selectors · inline("empty-@arr[] { }") → fragments inside values.
  • Combine freely with $var!, @define, num(), and modules (e.g. st-core chart polygons).

1. Property loop — %n()

Same value on multiple properties

Basic

Build a property-name array, store it, then multiplex one value across the list with %4, %3, %2, etc.

FSCSS
@arr sizing[width, height, max-width, max-height]
$sizing: @arr.sizing!;

.box {
  bg: red;
  %4($sizing.list[: 200px;])
}
Compiled CSS
.box {
  background: red;
  width: 200px;
  height: 200px;
  max-width: 200px;
  max-height: 200px;
}

Classic forms still work: %2(width, height[: 100px;]), %3(padding, margin, gap[: 1rem;]). See also value multiplexing on Examples and 1.2.5 property shorthands.

2. Auto-index / :nth-child loop

Staggered styles from parallel arrays

Intermediate

One rule template expands per index. Capture $index from the index array and look up delays/colors.

FSCSS
@arr delays[0.1s, 0.3s, 0.5s, 0.7s]
@arr colors[#ef4444, #f59e0b, #10b981, #3b82f6]
@arr indexes[count(@arr.colors!.length, 1)]   /* → 1,2,3,4 */

empty{ /* preserve */ }

.loading-dot:nth-child(@arr.indexes[]) {
  $index: @arr.indexes[];
  animation: bounce 1.5s infinite @arr.delays[$index!];
  background: @arr.colors[$index!];
}
Expands to
/* one rule per index */
.loading-dot:nth-child(1) {
  animation: bounce 1.5s infinite 0.1s;
  background: #ef4444;
}
.loading-dot:nth-child(2) { /* … */ }
/* … nth-child(3), nth-child(4) */

Also used for staggered keyframes and loading dots on the Examples · Arrays page.

3. Nested selector loop — rpt()

Classic “one-loop nested divs”

Intermediate

rpt(level, "div ") builds selector prefixes (div, div div, …). Pair with a parallel color (or token) array.

FSCSS
@arr colors[orange, purple, indigo, blue]
@arr levels[count(@arr.colors!.length)]

empty{ /* preserve */ }

rpt(@arr.levels[], "div ") {
  background: @arr.colors[@arr.levels[]];
  padding: 10px;
  border-radius: 10px;
  color: #fff;
  margin: 8px;
}
Compiled CSS
div {
  background: orange;
  padding: 10px;
  border-radius: 10px;
  color: #fff;
  margin: 8px;
}
div div { background: purple; /* … */ }
div div div { background: indigo; /* … */ }
div div div div { background: blue; /* … */ }

Deep dive: /nested-loop · HTML needs matching nested <div> depth.

4. Value fragments — inline()

Loops inside declarations

Advanced

inline() strips outer braces/format noise so loop output can sit inside a property value (custom properties lists, clip-path: polygon(...), etc.).

Custom properties from an array

@arr values[10, 20, 30, 40]
@arr idx[count(@arr.values!.length, 1)]

inline("empty{}
empty-@arr.idx[] {
  $i: @arr.idx[];
  --val-@arr.idx[]: @arr.values[$i!]px;
}")

Inside clip-path (chart-style)

clip-path: polygon(
  inline("{}
    empty-@arr.idx[] {
      $i: @arr.idx[];
      num(<$i - 1> * 100 / <@arr.values!.length - 1>)% var(--st-p$i),
    }
  ")
  100% 100%,
  0% 100%
);

Used heavily in st-core@v2 line/fill generators. Prefer empty-@arr… wrappers so loop machinery does not leak as selectors.

5. Combined starter

%n + rpt in one page

Practical
<script src="https://cdn.jsdelivr.net/npm/fscss@1.2.5/runtime.min.js" async></script>
<style>
@arr sizing[width, height, max-width, max-height]
$sizing: @arr.sizing!;

@arr colors[tomato, coral, salmon, crimson]
@arr levels[count(@arr.colors!.length)]

empty{ /* preserve */ }

.box {
  bg: red;
  %4($sizing.list[: 200px;])
}

rpt(@arr.levels[], "div ") {
  background: @arr.colors[@arr.levels[]];
  padding: 10px;
  border-radius: 10px;
  color: #fff;
}
</style>
<div class="box"></div>
<div><div><div><div>…</div></div></div></div>

6. empty{ /* preserve */ }

Loop expansion can temporarily introduce tokens that the parser treats as selectors. A leading:

empty{ /* preserve */ }

keeps the following loop blocks from “eating” surrounding structure. Required for many rpt / auto-index / inline patterns in 1.2.1+. Safe to leave in production sources — it compiles away cleanly when used as documented.

Cheat sheet

Construct Use for
@arr name[a, b, c] Declare list
count(n, 1) / count(@arr.x!.length) Index array 1…n
@arr.name[] Iterate all items
@arr.name[$i!] Lookup by index variable
%2 … %n(...list[: value;]) Property multiplexing
rpt(@arr.levels[], "div ") Repeated selector prefix
inline("…") Loop body inside a value
num(<expr>) Arithmetic in expansions