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)orcount(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
BasicBuild a property-name array, store it, then multiplex one value across the list with %4, %3, %2, etc.
@arr sizing[width, height, max-width, max-height]
$sizing: @arr.sizing!;
.box {
bg: red;
%4($sizing.list[: 200px;])
}
.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
IntermediateOne rule template expands per index. Capture $index from the index array and look up delays/colors.
@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!];
}
/* 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”
Intermediaterpt(level, "div ") builds selector prefixes (div, div div, …). Pair with a parallel color (or token) array.
@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;
}
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
Advancedinline() 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 |