Skip to content

Variables, data and repetition

View Markdownllms.txt

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:

FeatureWhat for
@letStore a value (text, number, true/false, list) and use it anywhere with $name.
@dataDeclare a data table, such as a list of people, plans or numbers.
::: forRepeat a section for each item in a list.
::: ifShow 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.

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

@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.

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

@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.

Inside $( … ) you can compute values:

@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))
OperationExample
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
Functionslen($list), min($list), max($list), sum($list), round($x), round($x, 1); in actions, random(1, 6) and pick($list) (Random draws)

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

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

@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.

::: 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:

@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}
:::
:::
:::
FormMeaning
::: for x in $listRepeats for each item.
::: for x, i in $listAlso gives the position $i, starting at 1.
::: for i in 1..5Repeats 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:

@let stages = [Discovery, Prototype, Pilot, Launch]
## How we get there
::: for stage, i in $stages
1. $stage {step=$i}
:::

::: 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:

@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:

@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.

chart and table accept data=$list:

@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"}
:::
AttributeWhereMeaningDefault
datachart, tableThe list of records(required)
labelchartThe field with the labelsThe first text field
serieschartThe fields with values, separated by spacesEvery numeric field
columnstableThe fields shown, in orderAll

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

@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**
:::
:::

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

@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).
  • .plan-$tone picks the style from the variant, without repeating properties.
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)

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

CodeWhen it happens
R601$name in an expression, if or for without being declared.
R602Types that don’t match, such as $text * 2 or an if on a number.
R603A badly written expression, such as $(1 +).
R606A for over something that is not a list, or too many repetitions.
R607::: else without a ::: if right before it.
R609A badly written @let, @state or @data.
R610The same name declared twice.