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 name principal, range 1000 to 100000, step 1000, starting value 10000, with a visible label
  • template stl: says "next is an STL output template"
  • {round(principal, 0)} is a mini-DSL expression — principal is the slider's current value, and round(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 (gain uses compound) — Rho MD figures out dependency order
  • The mini-DSL supports + - * / pow sqrt round min max sin cos pi log exp and similar pure math — note round(rate*100, 1) above: there are no format specifiers, so fixed decimals are done with round(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: computed lets 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: namespace lets 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: :::card shows literally, but the markdown inside the cards (bold, lists) renders normally
  • Interact blocks: show as syntax-highlighted code blocks with interact lang 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

Open in Rho MD →