Modal
Hide off-thread depth content behind a click-to-expand overlay — keep the main text compact while letting curious readers retrieve detail on demand.
When to use
- ✅ Optional depth ("expand to see derivation" math proofs)
- ✅ Large images / long code (don't bloat the page)
- ✅ History / footnotes / acknowledgments (don't affect the main thread but should be accessible)
- ✅ Risk disclosure / legal fine print (must exist but isn't featured)
- ✅ Media (videos / large images, save first-load bandwidth)
- ❌ Must-read content (anything readers must see while scanning) → use Callout
- ❌ Required forms (modal close discards state — bad for forms) → put Interact inline
- ❌ Core explanations (hiding makes readers miss it) → inline in main text
Basic syntax
\```modal trigger="show technical details"
Hidden content here —
- detailed algorithm
- edge cases
- performance analysis
\```
Renders: an inline "show technical details" link (underlined / with icon); click → centered overlay shows the modal content; click ✕ or background to close.
All options
Block-level: ```modal
| Option | Type | Default | Purpose |
|---|---|---|---|
trigger |
string | (required) | Text of the trigger link |
title |
string | trigger value | Title shown in modal header |
width |
enum | medium |
Modal width (small / medium / large / full) |
closeOnEsc |
bool | true |
Whether ESC closes |
closeOnOutsideClick |
bool | true |
Whether clicking outside the modal closes |
Quote
triggerif it has spaces:trigger="show details".
Examples
Example 1: optional math derivation
**Compound interest formula**: final value = principal × (1 + rate)^years
\```modal trigger="Math derivation (optional)"
**Why (1+r)^n?**
End of year 1 principal: P × (1+r)
End of year 2 principal: P × (1+r) × (1+r) = P × (1+r)²
End of year n principal: P × (1+r)ⁿ
**Why not P + n × r × P (simple interest)?**
Simple interest ignores "interest earning interest." The key in compound interest is that the (1+r) factor applies to each year's running total, not just the original principal.
\```
\```
*Renders: main text shows the clean formula; readers wanting derivation click "Math derivation (optional)" to expand. Beginners aren't drowned in proofs.*
### Example 2: large image
```markdown
System architecture overview (click for full diagram):
\```modal trigger="🖼️ Full architecture diagram (5000×3000, all microservices)" width=full

Legend:
- Blue: HTTP services
- Red: async queues
- Green: databases
- Purple: cache layer
\```
\```
*Renders: large image isn't shown inline (avoids slowing first paint); click triggers the modal to expand the full-width diagram + legend.*
### Example 3: long code / full script
```markdown
Quick integration:
\```js
import { hydrateAll } from '@rho/md';
hydrateAll(container);
\```
\```modal trigger="📜 Full setup code (with unified pipeline config)"
\```js
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import { registerAllInnerPlugins, remarkPlugins, hydrateAll } from '@rho/md';
import '@rho/md/css';
registerAllInnerPlugins();
const processor = unified()
.use(remarkParse)
.use(remarkGfm);
remarkPlugins.forEach((p) => processor.use(p));
processor
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStringify, { allowDangerousHtml: true });
const html = String(await processor.process(markdownText));
container.innerHTML = html;
hydrateAll(container);
\```
\```
\```
*Renders: main text shows the minimal integration; developers wanting full setup expand the modal to see 30 lines of config.*
### Example 4: legal / risk disclosure
```markdown
Signing up means you agree to our terms.
\```modal trigger="📄 Full Terms of Service + Privacy Policy" width=large
**Terms of Service (v3.2, 2026-05-14)**
Section 1. Service definition...
Section 2. User responsibilities...
Section 3. ...
**Privacy Policy (v2.1)**
We collect...
We will not...
\```
\```
*Renders: main text is a brief consent prompt; full terms expand on demand (satisfies "must exist + must be accessible" without overwhelming the signup flow).*
---
## Plain-text fallback behavior
Modal is a fenced code block ` ```modal `. In readers without support:
→ shows as a **code block with `modal` lang** — readers see the trigger text + all the content; **no "hidden" effect**.
Per-reader breakdown:
| Reader | Render |
|---|---|
| **Rho** | Trigger link + click pops up overlay |
| **GitHub web** | Code block (`modal` as lang label, trigger + all content as plain text) |
| **Obsidian** | Same |
| **VS Code default preview** | Same |
| **cat / less** | Plain text |
> ⚠️ **Special note**: because the fallback is "expand into a code block," **modal content is fully visible in non-supporting readers**. Good (readers don't lose content), but means: **don't use modal to hide private / sensitive info**. It's a UX-level "tidying" tool, not a security-level "hiding" tool.
---
## Common pitfalls
### 1. trigger text too short / unclear
```markdown
\```modal trigger="more"
the actual hidden complex content
\```
```
→ "more" doesn't tell readers what they'll see. **trigger should clearly describe "what'll expand"**:
- ✅ "View full derivation"
- ✅ "📜 Full setup code"
- ✅ "📄 Full Terms of Service"
- ❌ "more" / "click here" / "..." / "details"
### 2. Hiding core content in a modal
```markdown
\```modal trigger="how the algorithm works"
(Full algorithm explanation, 1000 words)
\```
```
→ Readers probably won't click — **they'll miss the core**.
**Rule**: if 70%+ readers need to see it, **don't modal it** — put it inline. Modal is for "optional depth," not "must-read but folded."
### 3. Modal inside modal
```markdown
\```modal trigger="Outer"
\```modal trigger="Inner" ← ❌ Modal in modal — overlay-on-overlay UX disaster
content
\```
\```
```
→ User clicks outer modal, finds another link to click for the second layer — **readers get lost.**
**Improvement**: inline the inner modal's content directly in the outer modal.
### 4. interact inside modal
```markdown
\```modal trigger="try this slider"
\```interact
slider x 0 10 5 1
template stl:
[Selected] -> [{x}]
\```
\```
```
✅ Technically supported by [Nested DSL](../advanced/nested-dsl.md). **But**:
- Closing and reopening the modal usually resets slider state
- Readers expect "demos" to be visible directly, not hidden in modals
- **For a core demo, don't modal it — put it in the main text**
### 5. Form in a modal expecting submission
```markdown
\```modal trigger="fill this form"
\```interact
input email ""
input name ""
\```
\```
```
→ Rho's interact state is **only in the reader's memory** — not persisted, not sent to a backend. **Closing the modal discards all input** — user filled it for nothing.
**Conclusion**: modals aren't for forms. Put forms inline + clearly state "demo only, no real submission."
### 6. trigger contains unescaped quotes
```markdown
\```modal trigger="what if you say "hello"" ← ❌ Nested quotes break parsing
```
→ Use full-width `"" ` or avoid nesting:
```markdown
\```modal trigger="what if you say \"hello\"" ← ✅ Escaped
\```modal trigger="what if you say "hello"" ← ✅ Full-width
\```
```
### 7. Short content in a modal
```markdown
\```modal trigger="click here"
Just one sentence.
\```
```
→ "Click → overlay → see one sentence → close" is more annoying than just reading the sentence. **Use modal only for content big enough to "bloat the page"** (≥ 5 lines / large image / long code / table).
---
## Modal vs Callout vs Tabs — how to choose
| Scenario | Use |
|---|---|
| Off-thread optional depth | **Modal** |
| Must-read emphasized passage | [Callout](../static-blocks/callout.md) |
| Multiple views, pick one (no hiding needed) | [Tabs](./tabs.md) |
| Strict tutorial-like sequence | [Stepper](./stepper.md) |
---
## See also
- [Callout](../static-blocks/callout.md) — Must-read emphasis (no hiding)
- [Tabs](./tabs.md) — Multiple views, same space (no hiding)
- [Stepper](./stepper.md) — Strict sequence
- [Nested DSL](../advanced/nested-dsl.md)
- [Plain-text fallback principle](../advanced/plain-text-fallback.md)