Conditionals & Loops
Show content based on conditions. Iterate over collections. Handle pagination.
If / Else
Use <if> to conditionally render content. The cond attribute accepts a variable or comparison expression.
<if user.authenticated>
<p>Welcome back, #user.full_name#!</p>
</if>
Add an <else/> block for the alternative:
<if user.authenticated>
<p>Welcome back, #user.full_name#!</p>
</if>
<else/>
<p>Please <a href="/login">log in</a> to continue.</p>
</else>
Chain multiple conditions with <elseif/>:
<if user.role == admin>
<span class="badge">Admin</span>
</if>
<elseif user.role == editor/>
<span class="badge">Editor</span>
</elseif>
<else/>
<span class="badge">Member</span>
</else>
Comparison Operators
The cond attribute supports these operators:
| Operator | Meaning | Example |
|---|---|---|
== | Equals | #status# == active |
!= | Not equals | #role# != guest |
> | Greater than | #count# > 0 |
< | Less than | #score# < 50 |
>= | Greater than or equal | #age# >= 18 |
<= | Less than or equal | #stock# <= 5 |
Simplified Syntax
You can omit the cond attribute and the #...# wrapping. Write conditions directly as the tag body:
<if active_step == 2>
<p>You're on step 2!</p>
</if>
<if count gt 0>
<p>There are items.</p>
</if>
<unless error>
<p>No errors found.</p>
</unless>
<if user.role == admin>
<span>Admin</span>
</if>
<elseif user.role == editor>
<span>Editor</span>
</elseif>
<else/>
<span>Member</span>
</else>
<if cond="#count# > 0"> and <if count gt 0> are equivalent. The simplified form is recommended.
Keyword Operators
Inside HTML tags, > and < would close the tag prematurely. Use keyword operators instead:
| Keyword | Equivalent | Meaning |
|---|---|---|
gt | > | Greater than |
lt | < | Less than |
gte | >= | Greater than or equal |
lte | <= | Less than or equal |
<if age gte 18>
<p>You can vote.</p>
</if>
<if stock lt 5>
<span class="badge">Low stock</span>
</if>
Without an operator, the condition is truthy — it passes if the variable exists and is not empty, false, 0, or null.
<!-- Truthy check: passes if flash.success has a value -->
<if flash.success>
<div class="alert">#flash.success#</div>
</if>
Prefix a truthy check with ! to negate it — it passes when the variable is empty, missing, or false:
<!-- Passes when no room is selected yet -->
<if !query.room>
<a href="/start">Pick a room</a>
</if>
Unless
The <unless> tag is an inverted <if>. It renders its content when the condition is false.
<unless errors>
<p>All fields are valid.</p>
</unless>
<unless user.authenticated>
<a href="/login">Log in</a>
</unless>
<unless> when you want to avoid a negated condition. It reads better than <if error != true>.
Loops
Use <loop> to iterate over arrays. The data attribute points to the collection, and as names the loop variable.
<what>
fetch.posts = "local:posts"
</what>
<loop data="#posts#" as="post">
<article>
<h2>#post.title#</h2>
<p>#post.excerpt#</p>
</article>
</loop>
Loops also work with objects, exposing #key# and #value# for each entry:
<loop data="#settings#" as="setting">
<dt>#key#</dt>
<dd>#setting#</dd>
</loop>
Nested Loops
Loops can be nested. Each loop has its own scope, so use distinct as names:
<loop data="#categories#" as="category">
<h2>#category.name#</h2>
<loop data="#category.products#" as="product">
<p>#product.name# — #product.price|currency#</p>
</loop>
</loop>
Loop Variables
Inside a loop, these variables are automatically available:
| Variable | Type | Description |
|---|---|---|
#index# | Number | Zero-based index (0, 1, 2, ...) |
#index1# | Number | One-based index (1, 2, 3, ...) |
#first# | Boolean | true for the first item |
#last# | Boolean | true for the last item |
#loop_total# | Number | Total number of items in the collection |
#loop_pages# | Number | Total number of pages (with pagination) |
#loop_page# | Number | Current page number (with pagination) |
<loop data="#users#" as="user">
<div class="user-row">
<span>#index1#. #user.name#</span>
<if first>
<span class="badge">First!</span>
</if>
<if last>
<span class="badge">Last!</span>
</if>
</div>
</loop>
Pagination
Add paginate and page attributes to a loop to split results across pages:
<loop data="#posts#" as="post" paginate="10" page="#query.page|default:1#">
<article>
<h2>#post.title#</h2>
<p>#post.excerpt#</p>
</article>
</loop>
<what-pagination/>
The paginate attribute sets items per page. The page attribute reads the current page from the query string (with a default of 1).
The <what-pagination/> component renders navigation links automatically — previous, next, page numbers, and ellipsis for large ranges.
<what-pagination/> is a built-in component. It reads the pagination state from the most recent paginated loop and generates accessible navigation with proper aria attributes.
Empty State
Handle empty collections by combining loops with conditionals:
<what>
fetch.tasks = "local:tasks"
</what>
<if tasks>
<loop data="#tasks#" as="task">
<div>#task.title#</div>
</loop>
</if>
<else/>
<p class="text-muted">No tasks yet. Create your first one!</p>
</else>
You can also use <unless> for a more direct approach:
<unless tasks>
<div class="empty-state">
<p>Nothing here yet.</p>
<a href="/tasks/new">Add your first task</a>
</div>
</unless>
<loop data="#tasks#" as="task">
<div>#task.title#</div>
</loop>