Variables, data and repetition
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.
Variables with @let
Section titled “Variables with @let”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 ascoral), a number (42,3.5),true/falseor a list ([Idea, Prototype, Launch]). - A word without
$is always text:@let mode = darkstores the textdark. - A
$namethat was not declared stays as plain text, soUS$ 10and$100do 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
Section titled “One variable controls many elements”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}## FastCompiles as you type. {.copy}:::::: box {.tile}## SafeNothing runs in the browser. {.copy}::::::Switch compact to true and every card gets smaller.
Expressions
Section titled “Expressions”Inside $( … ) you can compute values:
@let price = 49@let users = 12
## A team of $users peopleTotal: **$ $($price * $users)** per monthYearly, 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) |
There are no user-defined functions, endless loops or access to anything outside the document: expressions only compute values.
Data with @data
Section titled “Data with @data”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(oryes/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
Section titled “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:
@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:
@let stages = [Discovery, Prototype, Pilot, Launch]
## How we get there::: for stage, i in $stages1. $stage {step=$i}:::Conditions with ::: if
Section titled “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:
@let audience = executive
::: if $audience == technicalThe compiler runs in **wasm**, right in the browser.:::::: else if $audience == executiveWe cut slide building time by **70%**.:::::: elseWrite text, get slides.:::For a single element, use the if attribute:
@let draft = true
# Sales proposalPreliminary 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
Section titled “Charts and tables from data”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"}:::| 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
Section titled “Data-driven diagrams”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**::::::Components that take data
Section titled “Components that take data”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 >= 100Dedicated 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|darkdeclares the accepted values; the first one is the default. Any other value is an error (R418)..plan-$tonepicks the style from the variant, without repeating properties.
Component, variable or style?
Section titled “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) |
Tokens in the base theme
Section titled “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
Section titled “Common errors”| Code | When it happens |
|---|---|
| R601 | $name in an expression, if or for without being declared. |
| R602 | Types that don’t match, such as $text * 2 or an if on a number. |
| R603 | A badly written expression, such as $(1 +). |
| R606 | A for over something that is not a list, or too many repetitions. |
| R607 | ::: else without a ::: if right before it. |
| R609 | A badly written @let, @state or @data. |
| R610 | The same name declared twice. |