Home Documentation Templates Examples Showcase GitHub
Theme

Structured data · Core reference

Loops & conditions.

Nift has deliberately constrained control flow for rendering structured JSON data: @for(...){...} for arrays/objects and @if(...){...} with ordinary else if / else branches.

This is data-oriented control flow, not general scripting.

Nift supports pure value expressions for arithmetic, comparisons, logic and lazy ternaries, but deliberately has no assignment, mutable variables, user-defined functions or arbitrary code execution. The purpose is still to select, derive and repeat structured data at build time rather than become a general scripting runtime.

Loop over an array

{
  "articles": [
    {"title":"First","type":"article"},
    {"title":"Notes","type":"note"},
    {"title":"Third","type":"article"}
  ]
}
@json(data, 'data/articles.json')

@for(item : data.articles){
    <article>
        <h2>$[item.title]</h2>
    </article>
}

The binding is scoped to the loop. It does not leak into the rest of the page.

Loop over an object

{
  "social": {
    "github": {"label":"GitHub","url":"https://github.com/"},
    "docs":   {"label":"Docs","url":"/docs/"}
  }
}
@for((key, val) : data.social){
    <a data-kind="$[key]" href="$[val.url]">
        $[val.label]
    </a>
}

Object iteration preserves the document's stored member order unless you explicitly request sorting. The parenthesised pair makes key/value destructuring explicit.

Loop metadata with $[loop.*]

Every @for iteration gets a small read-only loop binding describing its position in the rendered iteration order:

ValueMeaning
$[loop.index]One-based position: 1, 2, 3, …
$[loop.index0]Zero-based position: 0, 1, 2, …
$[loop.first]true only for the first rendered iteration.
$[loop.last]true only for the last rendered iteration.
$[loop.length]Total number of items in the collection.
@for(item : data.navigation) {
    <a href="$[item.url]">$[item.title]</a>

    @if(!loop.last) {
        <span aria-hidden="true">·</span>
    }
}

The metadata is lexical. Nested loops get their own loop object while the inner loop runs, then Nift restores the outer loop metadata afterwards:

@for(section : data.sections) {
    <h2>$[loop.index]. $[section.title]</h2>

    @for(item : section.items) {
        <p>Item $[loop.index] of $[loop.length]: $[item.name]</p>
    }

    <p>Finished section $[loop.index] of $[loop.length].</p>
}
loop is reserved deliberately.

You cannot create a JSON alias or loop variable named loop, just as loop bindings cannot replace built-in metadata such as title. That means $[loop.index] can never become ambiguous because a template happened to bind unrelated user data under the same name.

Sort while iterating with by ... asc|desc

When the source order is not the display order you want, add a sort key to the loop header:

@for(post : data.posts by post.date desc) {
    <article>
        <h2>$[post.title]</h2>
        <time>$[post.date]</time>
    </article>
}

The syntax is:

@for(item : collection by item.field asc)  { ... }
@for(item : collection by item.field desc) { ... }

asc or desc is required when by is present. Without a by clause, Nift preserves the collection's stored order.

Numeric sorting

@for(product : data.products by product.price asc) {
    <p>$[product.name] — $[product.price]</p>
}

String sorting

@for(person : data.people by person.name asc) {
    <li>$[person.name]</li>
}

Sort scalar arrays too

@for(score : data.scores by score desc) {
    <li>$[score]</li>
}

Object iteration can sort by key or value data

@for((key, project) : data.projects by project.priority desc) {
    <h2 data-key="$[key]">$[project.title]</h2>
}

For object loops the sort expression must begin with either the key binding or value binding. This also makes alphabetical key ordering possible:

@for((key, value) : data.labels by key asc) {
    ...
}
Sorting is strict, stable and non-mutating.

Every sort key must be a number or every sort key must be a string. Mixed types, objects, arrays, booleans and null are rejected rather than coerced. Equal keys keep their original relative order. Nift computes an iteration order; it does not rearrange or mutate the loaded JSON document.

Loop metadata follows the sorted order

After sorting, loop.first, loop.last and the index values describe the order that is actually rendered:

@for(post : data.posts by post.date desc) {
    @if(loop.first) {
        <strong>Newest</strong>
    }
    <a href="$[post.url]">$[post.title]</a>
}

Conditions

@if(item.published){
    <p>Published</p>
}

Negation

@if(!item.draft){
    <a href="$[item.url]">$[item.title]</a>
}

Logical composition

Conditions compose with short-circuit && and ||, existing ! negation, and parentheses. Ordinary precedence applies: negation, comparisons, &&, then ||.

@if(post.published && !post.draft){ ... }

@if((post.featured || post.pinned) && post.published){ ... }

Logical operators short-circuit. An operand that is not needed to determine the result is not resolved.

Pure numeric expressions

$[...] can evaluate pure numeric expressions using +, -, *, / and integer-valued %, with unary signs, parentheses and conventional precedence. The same expression evaluator also supports comparisons, ! and short-circuit &&/||, so expressions can render derived numbers or booleans and are shared with conditions.

$[loop.index + 1]
$[(price * quantity) / 100]

@if(loop.index % 2 == 0){ ... }
@if(price * quantity >= 100 && post.published){ ... }

Arithmetic is numeric only: Nift does not use + for string concatenation or coerce strings, booleans, arrays or objects into numbers. Division/modulo by zero and modulo with fractional operands are build errors.

Scalar comparisons

Conditions support equality with ==/!= and ordering with <, <=, > and >=.

@if(item.type == "article"){
    <article>...</article>
}

@if(item.type != "note"){
    ...
}

@if(item.priority >= 3){
    ...
}

@if(item.price < 100){
    ...
}

@if(item.enabled == true){
    ...
}

@if(item.value == null){
    ...
}

Both sides of a comparison may also be JSON paths:

@if(item.type == site.featured_type){
    ...
}

@if(item.priority >= site.minimum_priority){
    ...
}
Ordering is strict and predictable.

Numbers compare numerically. Strings compare lexicographically. Both operands of <, <=, > or >= must be numbers or both must be strings; Nift does not coerce between JSON types. Booleans, null, arrays and objects are not orderable.

Lazy ternary rendering

Use $[condition ? true-branch : false-branch] when a small inline choice is clearer than a block @if. When the false branch should be empty, the shorthand $[condition ? true-branch] is equivalent to $[condition ? true-branch : '']. The condition uses exactly the same evaluator as @if. If the selected branch is a quoted string literal, Nift renders the decoded string value without its quote delimiters; otherwise the selected branch is parsed as ordinary Nift source, so directives such as @input(...) remain lazy and composable.

<span class="status $[post.featured ? 'featured' : 'standard']">
    $[post.title]
</span>

$[post.featured ? @input('partials/featured-badge.html')]

The unselected branch is inert: it does not resolve values, parse directives, register dependencies or requirements, or fail because it references something that would only matter in that branch.

else if and else

Branches use ordinary text syntax—there is no @else keyword:

@if(item.type == "article"){
    <article>...</article>
}
else if(item.type == "video"){
    <video>...</video>
}
else if(item.type == "gallery"){
    <section class="gallery">...</section>
}
else{
    <p>Unknown item type</p>
}

You can have as many else if branches as the template needs. A plain else is optional; when present, it is the unconditional fallback and ends the chain.

Truthiness

ValueCondition
truetrue
false, nullfalse
numberfalse for zero, true otherwise
stringfalse when empty, true otherwise
array/objectfalse when empty, true otherwise

Nesting

@for(section : data.sections){
    <section>
        <h2>$[section.title]</h2>

        @for(item : section.items){
            @if(item.visible){
                <a href="$[item.url]">
                    $[item.title]
                </a>
            }
        }
    </section>
}

Loop variables may be shadowed by a nested loop; Nift restores the outer binding when the inner loop finishes.

Skipped branches really are skipped

Nift only parses the selected branch. This is important for both behaviour and dependencies:

@if(site.use_legacy_navigation){
    @input('partials/legacy-navigation.html')
}
else{
    @input('partials/navigation.html')
}

Only the selected input participates in that page render and dependency set. Code in an unselected branch is not executed.

Why : instead of in?

The loop syntax intentionally uses punctuation:

@for(item : items){ ... }
@for((key, val) : object){ ... }

It stays visually compact, avoids pretending Nift has a larger keyword-based programming language, and behaves more predictably in generic syntax highlighters.

What conditions intentionally do not include

No assignment
No mutable variables
No user-defined functions
No implicit string/number coercion
No map/filter/reduce methods
No arbitrary code execution

If data needs substantial transformation, do that in the tool that owns the data, write JSON, then let Nift render the result.

Why there is no general query pipeline

@for remains focused on iteration and optional stable ordering. When you need an immutable transformed array first, Nift also provides composable collection operations such as @filter, @map, @sort and @slice. They use the same pure expression model and can feed directly into @for. See collection operations →

For substantial data transformation, use the tool that owns or prepares the data—TypeScript, Python, jq, a database query, an API/export step or another specialist tool—write the resulting JSON, then let Nift schema-check and render it. This keeps templates focused on data → output and keeps Nift's language small.

You may never need this page

Many excellent Nift projects can stay entirely on @content, @input and @pathto. Control flow exists for the point where structured data genuinely makes repetition or selection clearer than authored markup.