Nested blocks
All AINP blocks (callout / layout / tabs / stepper / modal / interact / timeline) can nest inside each other — put a callout in a layout card, an interact in a stepper step, tabs inside a modal, layout inside a timeline event ... composition is unbounded.
When to use nesting
- ✅ Composite content structure (each layout card has its own callout / code / interact)
- ✅ Tutorial-style demos (each stepper step contains a hands-on demo)
- ✅ Multi-view + hidden depth (tabs switch views, each tab has a modal for details)
- ✅ Roadmap with embedded features (timeline event with embedded demo)
- ❌ Nesting 4+ levels deep (visually confusing; readers get lost)
- ❌ Same-type self-nesting (tabs in tabs / stepper in stepper — UI confusion)
Core mechanism: containers re-parse their content
Every container block (callout / layout / tabs / stepper / modal / timeline) parses its own inner content as full markdown + AINP again:
Outer markdown parse
↓
Hits ```layout block
↓
Each :::card content inside the layout is **parsed as markdown + AINP again**
↓ Hits ```interact inside :::card
↓ The interact renders normally
Result: nesting just works — you don't need to think about "who parses first," only that the syntax is correct, and Rho MD handles it automatically.
Recommended nesting patterns
Pattern 1: Layout + Callout (recommended)
```layout grid cols=2
:::card accent=green
**Recommended**
> [!INFO]
> Plan A fits 90% of users; zero-config.
Detailed description...
:::
:::card accent=red
**Use with caution**
> [!WARN]
> Plan B requires self-hosting.
Detailed description...
:::
```
Cards provide layout, callouts provide semantic emphasis — distinct responsibilities; composition feels natural.
Pattern 2: Stepper + Interact (tutorial-style demos)
```stepper
:::step title="Step 1: feel the parameters"
Drag the slider to feel the parameter space:
```interact
slider rate range=0..0.2 step=0.01 default=0.05 label="rate"
template stl:
[Rate] -> [{round(rate*100, 1)}%]
```
:::
:::step title="Step 2: full formula"
Now add principal and years:
```interact
slider rate range=0..0.2 step=0.01 default=0.05 label="rate"
slider years range=1..30 step=1 default=10 label="years"
slider principal range=1000..100000 step=1000 default=10000 label="principal"
computed compound = principal * pow(1+rate, years)
template stl:
[Final value] -> [${round(compound)}]
```
:::
```
Tutorial-style walkthrough — each step has a hands-on demo; 10x more effective than pure prose.
Pattern 3: Tabs + Modal (multi-view + optional depth)
```tabs
:::tab title="Quick start"
3 steps:
1. Open a new document
2. Write a demo
3. Watch it render
```modal trigger="Full walkthrough (per-platform details)"
(Detailed steps...)
```
:::
:::tab title="Sharing"
Publish your doc to the web and send the link.
```modal trigger="Full publishing guide"
(Detailed publishing steps...)
```
:::
```
The main thread inside tabs is clean; depth lives in modals — readers expand on demand.
Pattern 4: Timeline + Layout
```timeline
:::event date="2026 Q1" title="✅ v0.3 — base capabilities"
```layout grid cols=2
:::card accent=blue
12 static / interactive capabilities
:::
:::card accent=green
Plugin-ecosystem baseline
:::
```
:::
:::event date="2026 Q2" title="✅ v0.6 — animation + SVG"
Added timer + computed + svg = 15 capabilities
:::
```
Timeline event with embedded layout for more structured display; higher information density than pure prose.
Combinations to avoid
1. Same-type self-nesting
```tabs
:::tab title=Outer
```tabs ← ⚠️ tabs in tabs
:::tab title=Inner
```
:::
```
Problem: two tab bars compete visually; readers can't tell outer from inner.
Improvement: lift the inner tabs out of the outer tab content, or use a different container (modal / accordion-style).
Same logic applies:
- ❌ stepper in stepper (two progress bars)
- ❌ timeline in timeline (two axes)
- ❌ modal in modal (overlay-on-overlay)
- ❌ layout in layout (multi-grid visual chaos)
2. Nesting 4+ levels
```layout
:::card
```stepper
:::step
```tabs
:::tab
```layout ← 4th-level layout, readers lose track
:::
```
:::
```
:::
```
:::
```
3 levels is the comfort ceiling. At 4 levels of visual nesting, readers can't see structure boundaries. Redesign — flatten to 2 levels, or break into multiple independent blocks.
3. interact in interact (sharing state requires namespace)
```interact
slider x range=0..10 step=1 default=5
```interact ← ❌ This isn't nesting; these are two independent interact blocks
slider y range=0..10 step=1 default=5
```
```
Problem: two interact blocks have isolated state by default — dragging one doesn't affect the other. Improvement: use Shared state (namespace):
```interact
namespace foo
slider x range=0..10 step=1 default=5
```
```interact
namespace foo
slider y range=0..10 step=1 default=5
template stl:
[x+y] -> [{x + y}]
```
Performance / render order
1. Re-parsing isn't free
Each nested level runs another markdown parse + block compilation. Deep or repeated → performance degrades.
Roughly:
- 1 level nesting: < 1ms increment
- 2 levels: ~2-5ms
- 3 levels: ~10-20ms
- 4 levels: ~50ms+, may feel laggy
Practical advice: keep ≤ 3 levels.
2. Cost of duplicate declarations
When nesting, don't redeclare the same control — share via namespace:
:::tab title="View A"
```interact
namespace data
slider rate range=0..0.15 step=0.01 default=0.05
template stl: [Rate] -> [{rate}]
```
:::
:::tab title="View B"
```interact
namespace data
template vega-lite: ← ✅ Reuses namespace, doesn't redeclare slider
{ ... uses {rate} ... }
```
:::
Plain-text fallback behavior
Nesting doesn't break plain-text fallback — every level falls back independently.
Example: layout containing a callout containing an interact, on GitHub web:
- Outer layout
:::cardshows as literal - Middle callout
[!INFO]renders as a GFM alert (GitHub supports it) - Inner interact shows as a code block (sliders + template visible)
Each level degrades by its own fallback rules; the whole stays readable.
Common pitfalls
1. Fenced code in fenced code — backtick conflict
```layout
:::card
```interact ← ❌ Conflicts with outer \```; markdown closes here
slider x range=0..10 step=1 default=5
```
:::
```
→ When nesting fenced code, use more backticks for the inner:
````layout ← 4 backticks
:::card
```interact ← 3 backticks (fewer than outer)
slider x range=0..10 step=1 default=5
```
:::
````
Or reverse — outer 3, inner 4. Rule: outer and inner backtick counts must differ.
2. Nesting destroys plain-text readability
```layout grid cols=4
:::card
```layout grid cols=3
:::card
```layout
:::card
content
:::
```
:::
```
:::
```
→ Even with valid syntax, plain-text readers see a mess of ::: and ``` markers, completely unable to understand. Keep nesting ≤ 3 levels + make each level's responsibility clear.
3. Namespace expectation mismatch from nesting
:::card
```interact
slider x range=0..10 step=1 default=5
```
:::
:::card
```interact
template stl: [x] -> [{x}] ← ❌ x not declared in this block
```
:::
→ interact blocks in different cards have isolated state by default. To share, use namespace. See Shared state.
4. Height changes when nested
```layout grid cols=2
:::card accent=blue
```interact
slider x range=0..10 step=1 default=5
template stl: [{x}]
```
:::
:::card accent=red
Fixed text
:::
```
→ Dragging the left card's slider may change its content height (template output line count varies) → the whole layout row's height jitters. Give the interact a fixed min-height or minimize template line-count variation.
5. timeline with stepper inside
```timeline
:::event date=2026 title="V1.0"
```stepper
:::step title="Feature 1"
:::
```
:::
```
→ Technically legal but semantically confusing — timeline is a time axis, stepper is a tutorial walkthrough; mixing them confuses readers. Redesign: lift the stepper out; timeline event just summarizes / links to the version's features.
Recommended mental model
Categorize container blocks as structural vs state containers:
| Class | Members | Nesting behavior |
|---|---|---|
| Structural containers | callout / layout / tabs / stepper / modal / timeline | Embed any block; the container re-parses them |
| State containers | interact | Don't nest (same-name vars conflict); share across blocks via namespace |
Structural ↔ State: free combination (callout containing interact, layout containing interact, stepper containing interact — all legal).
Structural ↔ Structural: can nest (layout containing callout, tabs containing modal), but avoid same-type nesting (tabs in tabs).
State ↔ State: share via namespace, don't nest.
See also
- Shared state (namespace) — Multiple interact blocks sharing state
- Plain-text fallback principle — Nesting still falls back
- Layout grid + cards
- Stepper
- Modal
- Tabs