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 :::card shows 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

Open in Rho MD →