Tabs
Pack multiple views into the same space — the reader clicks a tab to switch what's shown, saving vertical room. Best for "the same information in multiple expressions" (multi-language code / raw vs chart / desktop vs mobile vs API).
When to use
- ✅ Multi-language code samples (the same logic in JS / Python / Rust)
- ✅ Multiple views of the same data (raw JSON / table / chart)
- ✅ Multi-platform / environment comparison (macOS / Windows / Linux install steps; iOS / Android screenshots)
- ✅ API request / response comparison (curl / Node.js / Python call style)
- ❌ Peer independent content (no need to switch — show all together) → use Layout grid
- ❌ Strict sequence (step 1 → 2 → 3) → use Stepper
- ❌ Strong contrast (the user must see all options to decide) → use Layout; tabs make non-active items "invisible"
Basic syntax
```tabs
:::tab title=JavaScript
```js
console.log('hello');
```
:::
:::tab title=Python
```py
print('hello')
```
:::
:::tab title=Rust
```rust
println!("hello");
```
:::
```
Renders: a tab bar at the top, the first tab active by default; click another tab to switch the content area. All tabs share the same vertical space.
All options
Block-level: ```tabs
| Option | Type | Default | Purpose |
|---|---|---|---|
default |
int / string | 0 (first one) |
Index or title of the default-active tab |
Tab-level: :::tab
| Option | Type | Default | Purpose |
|---|---|---|---|
title |
string | (required) | Text on the tab bar. Keep short (< 20 chars) |
Quote
titleif it has spaces:title="My Tab".
Examples
Example 1: multi-language code
```tabs
:::tab title=JavaScript
```js
async function getUser(id) {
const r = await fetch(`/api/user/${id}`);
return r.json();
}
```
:::
:::tab title=Python
```py
import requests
def get_user(id):
return requests.get(f'/api/user/{id}').json()
```
:::
:::tab title=Go
```go
func GetUser(id string) (*User, error) {
resp, err := http.Get("/api/user/" + id)
// ...
}
```
:::
```
Renders: 3 tabs "JavaScript / Python / Go"; click to switch language. Readers see only the language they care about.
Example 2: rich content (callout, lists, etc.)
```tabs
:::tab title=macOS
1. Download `.dmg`
2. Drag to Applications
3. Launch
> [!INFO]
> Apple Silicon and Intel universal binary
:::
:::tab title=Windows
1. Download `.msi`
2. Double-click to install
3. Launch from the Start menu
> [!WARN]
> Requires Windows 10+
:::
:::tab title=Linux
```bash
sudo apt install acme # Debian/Ubuntu
sudo dnf install acme # Fedora
```
Or download `.AppImage` and run directly.
:::
```
Renders: install steps for a hypothetical tool ("acme"), one platform per tab. Tab content can hold any markdown — lists, code blocks, callouts, links — not just plain text.
Example 3: same data in 3 views
```tabs
:::tab title=Raw
```json
{"q1": 100, "q2": 130, "q3": 95, "q4": 180}
```
:::
:::tab title=Table
| Quarter | Revenue |
|---|---|
| Q1 | 100 |
| Q2 | 130 |
| Q3 | 95 |
| Q4 | 180 |
:::
:::tab title=Chart
```vega-lite
{
"data": {"values": [{"q":"Q1","v":100},{"q":"Q2","v":130},{"q":"Q3","v":95},{"q":"Q4","v":180}]},
"mark": "bar",
"encoding": {"x": {"field": "q"}, "y": {"field": "v", "type": "quantitative"}}
}
```
:::
```
Renders: the same quarterly revenue data in three views (JSON / table / bar chart). Readers switch on demand — table for precise reading, chart for trends, JSON for copying.
Example 4: default active tab
```tabs default="Production"
:::tab title=Development
Development environment config...
:::
:::tab title=Staging
Staging environment config...
:::
:::tab title=Production
**Production environment config** (default visible because it's the most critical)
:::
```
Renders: "Production" tab is active by default (because of default="Production"), not the first one.
Plain-text fallback behavior
:::tab is a markdown directive container. In readers without directive support:
:::tab title=JavaScript
content
:::
→ :::tab title=... and ::: show as literals; markdown inside the tab (code blocks, lists, links) renders normally. All tab content expands inline as a sequence — readers see every piece of information, only losing the "pick one" interactivity.
Per-reader breakdown:
| Reader | Render |
|---|---|
| Rho | Full tabs (top bar + switching) |
| GitHub web | :::tab title=... literal + tab contents inlined |
| Obsidian | Same (unless a directive plugin is installed) |
| VS Code default preview | Same |
| cat / less | Plain text |
All tab content stays readable — that's the heart of plain-text fallback. Worst case: all tab contents are stacked together, not an error.
Common pitfalls
1. Forgot title
```tabs
:::tab ← ❌ Missing title
content
:::
```
→ Tab bar shows blank or "Tab 1". title is required.
2. Title with spaces, no quotes
:::tab title=My Tab ← ❌ "Tab" gets parsed as the next modifier
:::tab title="My Tab" ← ✅
3. Too many tabs (> 5)
| Tab count | UX |
|---|---|
| 1-3 | Best |
| 4-5 | OK |
| 6-8 | Tab bar crowded; mobile may wrap or scroll |
| > 8 | Anti-pattern. Switch to Layout grid + cards showing all, or sidebar nav. |
4. Tabs with wildly uneven content depths
```tabs
:::tab title=A
A is very long: 5 paragraphs, 3 lists, nested callout, code block...
:::
:::tab title=B
B is one sentence.
:::
```
→ Switch to B and the area is empty; switch to A and it's packed. Visual jolt. Improvement: pad B to comparable depth; or switch to Layout grid so readers can compare side by side.
5. Missing ::: close
```tabs
:::tab title=A
A content
← ❌ Missing :::
:::tab title=B
B content
:::
```
→ The whole tabs block fails to parse. Every :::tab needs a paired ::: to close.
6. Nesting tabs (not recommended)
```tabs
:::tab title=Outer
```tabs ← ⚠️ Tabs in tabs; UI gets confusing fast
:::tab title=Inner
:::
```
:::
```
→ Technically supported by Nested DSL, but readers struggle to tell outer from inner tab boundaries. Improvement: lift inner tabs out to be peers, or use a stepper (each step contains tabs).
7. interact blocks inside tabs — fine, but watch state
:::tab title=Demo
```interact
slider x range=0..10 step=1 default=5
template stl:
[Selected] -> [{x}]
```
:::
✅ Legal. But switching away and back may reset the slider state to its initial value (reader-dependent). Don't hide a critical demo in a non-default tab.
Tabs vs Stepper vs Layout — how to choose
| Scenario | Use |
|---|---|
| Multiple views / languages / versions (reader picks one) | Tabs |
| Strict step sequence (walk through, with Prev/Next) | Stepper |
| Peer comparison (reader must see all together) | Layout grid |
| Time-ordered events (with dates) | Timeline |
See also
- Stepper — Strict-sequence walkthrough with Prev/Next
- Modal — Hidden content, on-demand reveal
- Layout grid + cards — Peer comparison, all-at-once
- Nested DSL — Tabs containing callout / interact / etc.
- Plain-text fallback principle