Your first interactive .md
By the end of this tutorial, you'll have a working compound-interest calculator — 3 sliders, auto-computed derived values, a live-redrawing growth chart. All from markdown. Zero JS. Estimated time: 10 minutes.
What you need
You're already reading this inside Rho MD — so you have everything you need. Create a new .md file in any folder and follow along; every snippet below renders live the moment you paste it in.
What we're building
A compound-interest calculator:
Input principal, annual rate, and years → live-display final value, net gain, growth curve.
When done it'll look like this (mock):
┌─────────────────────────────────────┐
│ Principal [────●────────] $10,000 │
│ Rate [──●──────────] 5.0% │
│ Years [──────●──────] 10 │
├─────────────────────────────────────┤
│ Compounded = $16,289 │
│ Net gain = $6,289 │
│ │
│ [growth curve chart, live] │
└─────────────────────────────────────┘
We build it incrementally — one capability at a time across 6 steps.
Step 1: start with plain markdown
First, write a non-interactive doc explaining compound interest. Create compound.md:
# Compound interest calculator
Compound interest adds last period's interest to the principal, so next period earns interest on a bigger base.
Formula:
```
final value = principal × (1 + rate)^years
```
Example: $10,000 principal, 5% annual rate, 10 years → final value ≈ $16,289.
Open it in Rho MD. It's just an article right now.
For a reader to estimate their own scenario, they'd need their own calculator. We'll fix that.
Step 2: highlight the formula with a callout
The formula is the core; surface it. Replace the formula paragraph with a Callout:
# Compound interest calculator
Compound interest adds last period's interest to the principal, so next period earns interest on a bigger base.
> [!INFO]
> **Formula**: final value = principal × (1 + rate)^years
Example: $10,000 principal, 5% annual rate, 10 years → final value ≈ $16,289.
Refresh — the formula is now in a blue highlight block, easy to spot when scanning.
What you learned: callouts add visual weight to "must-read" passages. See Callout.
Step 3: turn "principal" into a slider
Replace the dead $10,000 with a draggable slider. Add an interact block under the formula:
> [!INFO]
> **Formula**: final value = principal × (1 + rate)^years
```interact
slider principal range=1000..100000 step=1000 default=10000 label="principal"
template stl:
[Principal] -> [${round(principal, 0)}]
```
Refresh — there's now a draggable slider plus a live-updating line: "Principal = $10,000". Drag the slider → the number changes live.
Syntax breakdown:
slider principal range=1000..100000 step=1000 default=10000 label="principal"creates a slider: variable nameprincipal, range 1000 to 100000, step 1000, starting value 10000, with a visible labeltemplate stl:says "next is an STL output template"{round(principal, 0)}is a mini-DSL expression —principalis the slider's current value, andround(principal, 0)keeps it to 0 decimals. The$just before the braces is plain literal text
What you learned: interact block = controls + template. Controls drive the
{var}placeholders in the template. See Interact controls.
Step 4: add rate + years sliders + computed values
Extend the same interact block — add two more sliders + a computed derived value:
```interact
slider principal range=1000..100000 step=1000 default=10000 label="principal"
slider rate range=0..0.15 step=0.01 default=0.05 label="rate"
slider years range=1..30 step=1 default=10 label="years"
computed compound = principal * pow(1+rate, years)
computed gain = compound - principal
template stl:
[Principal] -> [${round(principal, 0)}] ::mod(rate="{round(rate*100, 1)}%", years="{years} years")
[Compounded] -> [${round(compound, 0)}]
[Net gain] -> [${round(gain, 0)}]
```
Refresh — now there are 3 sliders + 3 live-output lines. Drag any slider → all 3 lines recompute.
New concepts:
computed compound = principal * pow(1+rate, years)— derived variable, auto-tracks the source sliders- Computed values can depend on other computed values (
gainusescompound) — Rho MD figures out dependency order - The mini-DSL supports
+ - * / pow sqrt round min max sin cos pi log expand similar pure math — noteround(rate*100, 1)above: there are no format specifiers, so fixed decimals are done withround(expr, N)
⚠️ Note: the mini-DSL isn't JavaScript — it supports only safe pure-function math, no
eval, no IO, no DOM access. That's part of AINP's safety design.
What you learned:
computedlets interaction be more than "show input value" — it can actually compute. See Computed.
Step 5: add a live chart
Numbers are nice, but how steep is the long-term growth? Add a chart that redraws live as you change rate/years. Either swap the template from stl to vega-lite in the same block, or add a new block:
```interact
slider principal range=1000..100000 step=1000 default=10000 label="principal"
slider rate range=0..0.15 step=0.01 default=0.05 label="rate"
slider years range=1..30 step=1 default=10 label="years"
computed compound = principal * pow(1+rate, years)
computed gain = compound - principal
template stl:
[Principal] -> [${round(principal, 0)}] ::mod(rate="{round(rate*100, 1)}%", years="{years} years")
[Compounded] -> [${round(compound, 0)}]
[Net gain] -> [${round(gain, 0)}]
```
```interact
slider principal range=1000..100000 step=1000 default=10000 label="principal"
slider rate range=0..0.15 step=0.01 default=0.05 label="rate"
slider years range=1..30 step=1 default=10 label="years"
template vega-lite:
{
"data": {"sequence": {"start": 0, "stop": {years}, "step": 1, "as": "year"}},
"transform": [
{"calculate": "{principal} * pow(1+{rate}, datum.year)", "as": "value"}
],
"mark": {"type": "line", "point": true},
"encoding": {
"x": {"field": "year", "type": "quantitative", "title": "Year"},
"y": {"field": "value", "type": "quantitative", "title": "Amount ($)"}
}
}
```
Refresh — the second block is a growth curve, year on x, amount on y. Drag any slider → the curve redraws.
Problem: sliders are written twice (once per block), and the two blocks' sliders are independent — dragging one block's slider doesn't affect the other block's chart.
Next step fixes that.
Step 6: share state with namespace
Add a namespace finance line as the first line inside each block to make both blocks share the same variables:
```interact
namespace finance
slider principal range=1000..100000 step=1000 default=10000 label="principal"
slider rate range=0..0.15 step=0.01 default=0.05 label="rate"
slider years range=1..30 step=1 default=10 label="years"
computed compound = principal * pow(1+rate, years)
computed gain = compound - principal
template stl:
[Principal] -> [${round(principal, 0)}] ::mod(rate="{round(rate*100, 1)}%", years="{years} years")
[Compounded] -> [${round(compound, 0)}]
[Net gain] -> [${round(gain, 0)}]
```
```interact
namespace finance
template vega-lite:
{
"data": {"sequence": {"start": 0, "stop": {years}, "step": 1, "as": "year"}},
"transform": [
{"calculate": "{principal} * pow(1+{rate}, datum.year)", "as": "value"}
],
"mark": {"type": "line", "point": true},
"encoding": {
"x": {"field": "year", "type": "quantitative", "title": "Year"},
"y": {"field": "value", "type": "quantitative", "title": "Amount ($)"}
}
}
```
Note the second block has only template — no sliders / computed — it reuses all the variables defined in the first block of namespace finance.
Refresh — drag any slider, text and chart update together.
What you learned:
namespacelets multiple interact blocks share state, avoiding duplicate definitions. See Shared state.
Step 7 (optional): use layout to make it look polished
Finally, wrap controls + outputs + chart in a 2-column Layout grid + cards:
# Compound interest calculator
> [!INFO]
> **Formula**: final value = principal × (1 + rate)^years
```layout grid cols=2
:::card accent=blue
**Inputs**
```interact
namespace finance
slider principal range=1000..100000 step=1000 default=10000 label="principal"
slider rate range=0..0.15 step=0.01 default=0.05 label="rate"
slider years range=1..30 step=1 default=10 label="years"
computed compound = principal * pow(1+rate, years)
computed gain = compound - principal
template stl:
[Principal] -> [${round(principal, 0)}]
[Compounded] -> [${round(compound, 0)}]
[Net gain] -> [${round(gain, 0)}]
```
:::
:::card accent=green
**Growth curve**
```interact
namespace finance
template vega-lite:
{
"data": {"sequence": {"start": 0, "stop": {years}, "step": 1, "as": "year"}},
"transform": [{"calculate": "{principal} * pow(1+{rate}, datum.year)", "as": "value"}],
"mark": {"type": "line", "point": true},
"encoding": {"x": {"field": "year"}, "y": {"field": "value", "type": "quantitative"}}
}
```
:::
```
Refresh — now it looks like a product: blue card on the left for controls + outputs, green card on the right with the chart, the page clean and symmetric.
What you learned: any block nests in any block — layout in interact, callout in interact, stepper in layout all work. See Nested DSL.
What you actually built
In 10 minutes you used 5 of Rho's capabilities:
| Capability | Where |
|---|---|
Callout (> [!INFO]) |
Formula highlight |
| Layout grid + cards | Two-column layout |
| Interact controls (slider) | 3 sliders |
| Interact text template (stl) | Text output |
| Interact + Vega-Lite | Growth curve |
| Computed | compound / gain derivation |
Shared state (namespace) |
Text and chart blocks share variables |
| Nested DSL | layout containing interact |
The whole document has no JS — it's pure declarative. Anyone who knows markdown can edit it.
Plain-text fallback verification
Open compound.md in a non-Rho reader (GitHub web / VS Code default preview / cat):
- Formula callout: still renders on GitHub (GFM-compatible standard callout)
- Layout cards:
:::cardshows literally, but the markdown inside the cards (bold, lists) renders normally - Interact blocks: show as syntax-highlighted code blocks with
interactlang label — readers see sliders / computed / template, enough to understand the original was interactive
The reader never sees a broken render — that's Rho's core promise.
What's next
| Want to… | Entry |
|---|---|
| Play with this example more | Change chart type, add a toggle for two scenarios, add a timer for auto-play... |
| See more demos (30+ complete examples) | → Examples gallery |
| Learn each capability systematically | → Cheatsheet — every capability, one page |
| Look up specific syntax | → the AINP protocol |
| Have AI generate docs for you | → the built-in Rho AI plugin speaks AINP natively |
See also
- What is Rho? — Philosophical background
- 5-minute tour — All 15 capabilities
- Callout
- Layout grid + cards
- Interact controls
- Computed
- Vega-Lite live chart
- Shared state (namespace)
- Nested DSL