Interact — controls
Declare interactive controls inside an
```interactblock — slider / timer / input / chip / select / toggle. Readers manipulate the controls; the controls drive live output in the same or other blocks. This is the core engine of all dynamic content in Rho MD.
When to use
- ✅ Parameterized display (financial calculators / physics simulations / color palette pickers / API call sandboxes)
- ✅ Visualizing teaching formulas (drag a slider, see how the formula changes)
- ✅ Lightweight calculators (BMI / compound interest / unit conversion / mortgage)
- ✅ Data exploration (slider tweaks → chart redraws — pair with Vega-Lite chart)
- ✅ Auto-playing animation (drive a scene or chart with a
timerinstead of manual dragging) - ❌ Real form submission (state lives only in reader memory; not persisted)
- ❌ Complex business logic (the mini DSL isn't JS — no
if/else/loop) - ❌ Cross-page state sharing (state is per-page only)
Basic syntax
An interact block has two parts: control declarations + template output.
```interact
slider x range=0..100 step=1 default=50
template stl:
[Current value] -> [{x}]
```
Renders: a slider (min 0, max 100, initial 50, step 1) + a live line "Current value = 50". Drag → number changes.
All control types
Rho MD provides 6 control types:
| Control | Use | Syntax |
|---|---|---|
| slider | Numeric slider | slider name range=A..B step=S default=V label="..." |
| timer | Auto-playing numeric driver | timer name range=A..B step=S speed=FPS loop=true|false |
| input | Text input | input name default="..." placeholder="..." label="..." |
| chip | Segmented choice | chip name options=a,b,c default=a label="..." |
| select | Dropdown | select name options=a,b,c default=a label="..." |
| toggle | Boolean switch | toggle name default=true label="..." |
All attributes are key=value pairs — there is no positional-argument form for any control.
Each control in detail
slider
slider <name> range=<A>..<B> step=<S> default=<V> label="..."
| Attribute | Type | Purpose |
|---|---|---|
name |
identifier | Variable name (referenced as {name} in template) |
range |
A..B |
Minimum and maximum, joined by .. |
step |
number | Step size (per-drag increment) |
default |
number | Initial value (must be in [A, B]) |
label |
string | Optional display label |
slider rate range=0..0.2 step=0.01 default=0.05 label="Rate" ← 0%-20%, 1% step
slider weight range=40..200 step=0.5 default=70 label="Weight (kg)"
slider temperature range=-20..40 step=1 default=20 label="Temp (°C)"
timer
timer <name> range=<A>..<B> step=<S> speed=<FPS> loop=true|false
| Attribute | Type | Purpose |
|---|---|---|
name |
identifier | Variable name (referenced as {name} in template) |
range |
A..B |
Minimum and maximum the timer sweeps through |
step |
number | Increment per tick |
speed |
number | Ticks per second (FPS) |
loop |
bool | true wraps around at B and repeats; false runs through a single pass and stops at B |
timer t range=0..10 step=0.1 speed=30 loop=true ← loops forever
timer intro range=0..1 step=0.02 speed=60 loop=false ← single pass, 0 → 1
There is no separate mode for pausing, playing a single pass, or reversing direction —
loop=falseis the only variant, and it is how you get a one-shot animation.
input
input <name> default="..." placeholder="..." label="..."
| Attribute | Type | Purpose |
|---|---|---|
name |
identifier | Variable name |
default |
string | Default content |
placeholder |
string | Optional placeholder text (shown when empty) |
label |
string | Optional display label |
input email default="you@example.com" label="Email"
input city default="Shanghai" label="City"
input apiKey default="" placeholder="sk-..." label="API key" ← Empty default
input accepts arbitrary text — pure string, no format validation. For numeric input, use
sliderinstead.
chip
chip <name> options=<a,b,c> default=<a> label="..."
| Attribute | Type | Purpose |
|---|---|---|
name |
identifier | Variable name |
options |
comma-separated list | Choices, shown as segmented chips |
default |
string | Which option is initially selected (must be one of options) |
label |
string | Optional display label |
chip size options=S,M,L,XL default=M label="Size"
chip theme options=dark,light,system default=dark label="Theme"
select
select <name> options=<a,b,c> default=<a> label="..."
| Attribute | Type | Purpose |
|---|---|---|
name |
identifier | Variable name |
options |
comma-separated list | Options; a dropdown |
default |
string | Initially selected option (must be one of options) |
label |
string | Optional display label |
select theme options=dark,light,system default=dark label="Theme"
select unit options=metric,imperial default=metric label="Units"
select region options=us-east,us-west,eu-west,ap-northeast default=us-east label="Region"
chipandselectcarry the same data shape (one string out of a fixed option list) — pick whichever reads better in the layout; chip suits 2-4 short options, select suits longer lists.
toggle
toggle <name> default=true|false label="..."
| Attribute | Type | Purpose |
|---|---|---|
name |
identifier | Variable name |
default |
bool | Initial state |
label |
string | Optional display label |
toggle darkMode default=false label="Dark mode"
toggle notifications default=true label="Notifications"
toggle showAdvanced default=false label="Show advanced"
toggle's value is the string
"true"/"false", not a real boolean. For conditional display, compare it explicitly:{darkMode == 'true' ? 'Dark' : 'Light'}.
Examples
Example 1: simplest slider
```interact
slider x range=0..10 step=1 default=5
template stl:
[Selected value] -> [{x}]
```
Renders: slider (0-10, initial 5), one line "Selected value = 5" below.
Example 2: multi-control combo (parameter panel)
```interact
slider weight range=40..120 step=1 default=70 label="Weight"
input name default="Alice" label="Name"
select activity options=sedentary,moderate,active default=moderate label="Activity"
toggle metric default=true label="Metric units"
template stl:
[Name] -> [{name}]
[Weight] -> [{weight} kg]
[Activity] -> [{activity}]
[Metric units] -> [{metric}]
```
Renders: 4 different control types (slider + text + dropdown + toggle) stacked at top; 4 live output lines below.
Example 3: chip-driven step counter
```interact
chip step options=1,2,3,5,10 default=1 label="Step size"
slider count range=0..100 step=1 default=0 label="Count"
computed next = count + step
template stl:
[Count] -> [{count}]
[Next tick (+{step})] -> [{next}]
```
Renders: a chip to pick the step size + a slider for the current count; one line previews what the count would be after adding the chosen step.
The 6-control set has no discrete click-trigger widget — every control is a continuous input (drag / type / pick). Where older docs showed a click-triggered "+1 counter" pattern, drive the same idea with a
sliderorchipinstead, as above.
Example 4: select + toggle for display mode switch
```interact
select unit options=metric,imperial default=metric label="Unit"
slider weight range=40..120 step=1 default=70 label="Weight"
computed display = unit == 'metric' ? weight : weight * 2.20462
computed unitLabel = unit == 'metric' ? 'kg' : 'lb'
template stl:
[Weight] -> [{round(display, 1)} {unitLabel}]
```
Renders: dropdown selects metric/imperial + slider; one line shows 70 kg or 154.3 lb based on the unit.
The mini DSL supports the ternary
cond ? a : b, but not if/else statements. There are no format specifiers either — fixed-decimal output goes throughround(expr, N), as shown above.
Plain-text fallback behavior
```interact is a fenced code block with interact lang. In readers without support:
```interact
slider x range=0..10 step=1 default=5
template stl:
[Selected value] -> [{x}]
```
→ shows as a syntax-highlighted code block — readers see all sliders / inputs / template declarations, fully understand the original document's intent (including range/default/step attributes).
Per-reader breakdown:
| Reader | Render |
|---|---|
| Rho MD | Full interaction (controls + live template output) |
| GitHub web | Code block (slider/input/select literals + template literal all visible) |
| Obsidian | Same |
| VS Code default preview | Same |
| cat / less | Plain text |
Information preserved — readers see the parameter space + template structure; they just can't drag.
Common pitfalls
1. Positional arguments instead of attributes
slider x <min> <max> <default> <step> ← ❌ positional form is not parsed
slider x range=100..0 step=1 default=50 ← ❌ min > max, undefined behavior
slider x range=0..100 step=1 default=1000 ← ❌ default > max
slider x range=0..100 step=0 default=50 ← ❌ step=0, can't drag
slider x range=0..100 step=1 default=50 ← ✅
Every control takes key=value attributes, not positional min/max/initial/step. range is min ≤ default ≤ max; step > 0.
2. Variable name with spaces / special chars
slider my var range=0..10 step=1 default=5 ← ❌ breaks parsing
slider my-var range=0..10 step=1 default=5 ← ⚠️ hyphens rejected
slider my_var range=0..10 step=1 default=5 ← ✅
slider myVar range=0..10 step=1 default=5 ← ✅
Variable names: only [A-Za-z0-9_], like standard programming language identifiers.
3. Using undeclared variables in template
```interact
slider x range=0..10 step=1 default=5
template stl:
[Result] -> [{y}] ← ❌ y not declared
```
→ Template renders {y} as literal {y} or throws "undefined variable" error. Variables in template must first be declared via slider/timer/input/chip/select/toggle/computed.
4. chip / select options with special chars
select theme options=dark,light default=dark ← ✅
chip size options=S,M,L,XL default=M ← ✅
select theme options="dark mode,light mode" default="dark mode" ← ⚠️ multi-word options need care with commas
Options are a bare comma-separated list — don't wrap the whole list in quotes; avoid commas inside individual option values.
5. toggle compared as a boolean instead of a string
computed label = darkMode ? "Dark" : "Light" ← ❌ darkMode is the string "true"/"false", not a real bool
computed label = darkMode == 'true' ? 'Dark' : 'Light' ← ✅
toggle's value is always the string "true" or "false" — compare it explicitly with == 'true'.
6. input takes a number but treated as string
```interact
input age default="30" label="Age"
template stl:
[Age+1] -> [{age + 1}] ← ❌ "30" + 1 is not numeric addition — input is a string
```
→ input value is always a string, meant for free text substituted verbatim into the template. For numeric values, use slider (or timer) instead of input.
7. Too many controls
```interact
slider a range=0..10 step=1 default=5
slider b range=0..10 step=1 default=5
slider c range=0..10 step=1 default=5
slider d range=0..10 step=1 default=5
slider e range=0..10 step=1 default=5
slider f range=0..10 step=1 default=5
... (12 sliders)
template stl:
[Result] -> [{a + b + c + d + e + f + ...}]
```
→ Above 6-7 controls, UI gets crowded and readers lose track. Improvements:
- Use Layout grid to group controls into cards
- Split into multiple interact blocks (share variables via Shared state namespace)
- Reconsider: do you really need this many controls?
Where to next
| Want to… | Entry |
|---|---|
| Learn template syntax | → Interact text template |
| Add derived computations (not just echo input) | → Computed |
| Add live charts (Vega-Lite) | → Interact + Vega-Lite |
| Share variables across multiple interact blocks | → Shared state (namespace) |
| Add auto-playing animation | → Timer |
| Draw SVG scenes | → SVG scenes |
See also
- Interact text template — Template syntax details
- Computed — Derived values
- Interact + Vega-Lite chart — Slider-driven live charts
- Timer — Auto-play (replaces manual dragging)
- Shared state (namespace)
- Plain-text fallback principle