# Variables, data and repetition

> Use @let, @data, ::: for and ::: if to build pages from values and lists without repeating yourself.

When the same information shows up in several places, or a page is made of many similar items, writing everything by hand is tedious and error-prone. Ribalta solves this with four features that work together:

| Feature | What for |
| --- | --- |
| `@let` | Store a value (text, number, true/false, list) and use it anywhere with `$name`. |
| `@data` | Declare a data table, such as a list of people, plans or numbers. |
| `::: for` | Repeat a section for each item in a list. |
| `::: if` | Show a section only when a condition is true. |

All of this is resolved **before** the document is shown: the compiler builds the final pages and reports any problem. For content that changes **while** the document is shown (on click, for example), see [Interactivity and navigation](https://viniciusadm.github.io/ribalta/en/guides/interactive/).

## Variables with `@let`

Declare the variable at the start of the document, together with the other definitions, and use `$name` in text or attributes:

```ribalta
@let event = "Annual Meeting 2026"
@let highlight = coral
@let spacing = lg

Invitation {.kicker}
# $event {color=$highlight}
::: row {gap=$spacing}
:: icon calendar {color=$highlight}
Sign up by Friday
:::
```

- Names use lowercase letters, numbers, hyphens and underscores, starting with a letter.
- The value can be text (`"with spaces"` in quotes, or a single word such as `coral`), a number (`42`, `3.5`), `true`/`false` or a list (`[Idea, Prototype, Launch]`).
- A word without `$` is always text: `@let mode = dark` stores the text `dark`.
- A `$name` that was not declared stays as plain text, so `US$ 10` and `$100` do not change.

For colors, `@color` is still the best choice. Use `@let` for everything else: sizes, spacing, repeated text and options that turn parts of the document on or off.

### One variable controls many elements

Combine `@let` with `@style` to change the look of the whole document by switching a single value:

```ribalta
@let compact = false
@let spacing = $compact ? sm : lg
@let body = $compact ? md : xl
@style tile {.card pad=$spacing gap=$spacing}
@style copy {size=$body}

::: grid {cols=2 gap=$spacing}
::: box {.tile}
## Fast
Compiles as you type. {.copy}
:::
::: box {.tile}
## Safe
Nothing runs in the browser. {.copy}
:::
:::
```

Switch `compact` to `true` and every card gets smaller.

### Expressions

Inside `$( … )` you can compute values:

```ribalta
@let price = 49
@let users = 12

## A team of $users people
Total: **$ $($price * $users)** per month
Yearly, with 15% off: $ $(round($price * $users * 12 * 0.85))
```

| Operation | Example |
| --- | --- |
| Arithmetic | `$a + 1`, `$price * 12`, `$total / 2`, `$n % 2` |
| Join text | `"Hello, " + $name` |
| Comparison | `$a == $b`, `$a != pro`, `$n > 10`, `$n <= 3` |
| In a list | `$fruit in [apple, pear]`, `key in $backpack` |
| Logic | `$a and $b`, `$a or $b`, `not $a` |
| Choice | `$compact ? sm : lg` |
| Functions | `len($list)`, `min($list)`, `max($list)`, `sum($list)`, `round($x)`, `round($x, 1)`; in actions, `random(1, 6)` and `pick($list)` ([Random draws](https://viniciusadm.github.io/ribalta/en/guides/interactive/#random-draws)) |

There are no user-defined functions, endless loops or access to anything outside the document: expressions only compute values.

## Data with `@data`

A table between `@data name` and `@end` becomes a list of records. The first row names the fields:

```ribalta
@data team
| name  | role        | color |
| ---   | ---         | ---   |
| Ana   | CTO         | blue  |
| Bruno | Design      | coral |
| Caio  | Engineering | green |
@end
```

- Columns with only numbers become numbers; `true`/`false` (or `yes`/`no`) become true/false; everything else is text.
- Each row is read with `$item.field`, such as `$person.name`.
- A field of a whole list returns the column: `max($sales.total)` is the largest total.

## Repetition with `::: for`

`::: for item in $list` repeats the content for each item. The generated blocks go straight into the parent block, so they work as grid cells, columns or steps:

```ribalta
@data team
| name  | role        | color |
| ---   | ---         | ---   |
| Ana   | CTO         | blue  |
| Bruno | Design      | coral |
| Caio  | Engineering | green |
@end

## Our team
::: grid {cols=3 gap=lg}
::: for person in $team
::: box {.card align=center}
:: icon user {size=3xl color=$person.color}
### $person.name
$person.role {.muted}
:::
:::
:::
```

| Form | Meaning |
| --- | --- |
| `::: for x in $list` | Repeats for each item. |
| `::: for x, i in $list` | Also gives the position `$i`, starting at 1. |
| `::: for i in 1..5` | Repeats for the numbers 1 to 5. |

Markdown lists generated by a `for` are merged into a single list with the right numbering. Use the position to reveal one item at a time:

```ribalta
@let stages = [Discovery, Prototype, Pilot, Launch]

## How we get there
::: for stage, i in $stages
1. $stage {step=$i}
:::
```

## Conditions with `::: if`

`::: if condition` shows the content only when the condition is true. Right after it, `::: else if` and `::: else` give alternatives. Each block closes with `:::`, like any other:

```ribalta
@let audience = executive

::: if $audience == technical
The compiler runs in **wasm**, right in the browser.
:::
::: else if $audience == executive
We cut slide building time by **70%**.
:::
::: else
Write text, get slides.
:::
```

For a single element, use the `if` attribute:

```ribalta
@let draft = true

# Sales proposal
Preliminary version, subject to review {.muted if=$draft}
```

Like `for`, `if` adds no extra box: the chosen content goes into the parent block.

## Charts and tables from data

`chart` and `table` accept `data=$list`:

```ribalta
@data sales
| month | revenue | cost |
| ---   | ---     | ---  |
| Jan   | 120     | 80   |
| Feb   | 150     | 90   |
| Mar   | 210     | 110  |
@end

::: columns {gap=xl}
:: chart {data=$sales type=line}
:: table {data=$sales columns="month revenue"}
:::
```

| Attribute | Where | Meaning | Default |
| --- | --- | --- | --- |
| `data` | `chart`, `table` | The list of records | (required) |
| `label` | `chart` | The field with the labels | The first text field |
| `series` | `chart` | The fields with values, separated by spaces | Every numeric field |
| `columns` | `table` | The fields shown, in order | All |

## Data-driven diagrams

You don't need a special block to draw from data: a `for` with boxes of computed width does the job.

```ribalta
@data adoption
| month | teams |
| ---   | ---   |
| Jan   | 120   |
| Feb   | 340   |
| Mar   | 910   |
@end

## Teams using the product
::: for m, i in $adoption
::: row {gap=md valign=center step=$i}
$m.month {w=80 .muted}
::: box {w="$(round($m.teams / max($adoption.teams) * 100))%" h=40 bg=accent radius=sm}
:::
**$m.teams**
:::
:::
```

## Components that take data

A component parameter can take a whole record, and the component body can use `if` and `for` on its parameters:

```ribalta
@data plans
| id    | name  | price | users |
| ---   | ---   | ---   | ---   |
| basic | Basic | 0     | 3     |
| pro   | Pro   | 49    | 25    |
| team  | Teams | 199   | 200   |
@end

@component plan {p tone=light|dark}
::: box {.card align=center .plan-$tone}
### $p.name
$ $p.price {size=3xl weight=black}
::: if $p.users >= 100
Dedicated support {.kicker}
:::
:::
@end
@style plan-light {bg=surface}
@style plan-dark {bg=ink color=paper}

::: grid {cols=3 gap=lg}
::: for p in $plans
:: plan {p=$p tone="$($p.id == pro ? dark : light)"}
:::
:::
```

- `tone=light|dark` declares the accepted values; the first one is the default. Any other value is an error ([R418](https://viniciusadm.github.io/ribalta/en/reference/errors/)).
- `.plan-$tone` picks the style from the variant, without repeating properties.

### Component, variable or style?

| I want… | Use |
| --- | --- |
| A bundle of visual properties | `@style` |
| The same structure with different content | `@component` |
| A shared value (size, text, option) | `@let` |
| Several structures generated from data | `@data` + `::: for` + component |
| Something that changes while the document is shown | `@state` (see [Interactivity](https://viniciusadm.github.io/ribalta/en/guides/interactive/)) |

## Tokens in the base theme

The base theme also accepts `@let` and `@data`. A document can redefine a theme `@let`, just like it does with `@color`.

## Common errors

| Code | When it happens |
| --- | --- |
| [R601](https://viniciusadm.github.io/ribalta/en/reference/errors/) | `$name` in an expression, `if` or `for` without being declared. |
| [R602](https://viniciusadm.github.io/ribalta/en/reference/errors/) | Types that don't match, such as `$text * 2` or an `if` on a number. |
| [R603](https://viniciusadm.github.io/ribalta/en/reference/errors/) | A badly written expression, such as `$(1 +)`. |
| [R606](https://viniciusadm.github.io/ribalta/en/reference/errors/) | A `for` over something that is not a list, or too many repetitions. |
| [R607](https://viniciusadm.github.io/ribalta/en/reference/errors/) | `::: else` without a `::: if` right before it. |
| [R609](https://viniciusadm.github.io/ribalta/en/reference/errors/) | A badly written `@let`, `@state` or `@data`. |
| [R610](https://viniciusadm.github.io/ribalta/en/reference/errors/) | The same name declared twice. |
