Remark plugins
Detailed reference for
@rho/md's 7 build-time remark plugins — when to use each, what each does, what HTML each emits.
Overview
| Plugin | Handles DSL | Output HTML marker class | Needs hydration? |
|---|---|---|---|
remarkCallout |
Callout (> [!INFO]) |
<aside class="rho-callout rho-callout-info"> |
❌ Static |
remarkLayout |
Layout (```layout) |
<div class="rho-layout-grid" data-cols="3"> |
❌ Static |
remarkAnnotate |
Annotate (```annotate) |
<div class="rho-annotate"> + <span class="rho-anno"> |
❌ Static (hover tooltip via CSS) |
remarkTabs |
Tabs (```tabs) |
<div class="rho-tabs"> |
✅ renderTabsBlocks |
remarkStepper |
Stepper (```stepper) |
<div class="rho-stepper"> |
✅ renderStepperBlocks |
remarkTimeline |
Timeline (```timeline) |
<ol class="rho-timeline"> |
❌ Static |
remarkModal |
Modal (```modal) |
<a class="rho-modal-trigger"> + <dialog class="rho-modal"> |
✅ renderModalBlocks |
interact blocks (with vega / svg / template / computed / timer) aren't in remark plugins — their fenced
```interactblocks are converted by unified default to<pre><code>, thenrenderInteractBlocksrecognizes by lang label and takes over.
All-vs-individual registration
Register all (recommended)
import { remarkPlugins } from '@rho/md';
remarkPlugins.forEach((p) => processor.use(p));
It's the array [remarkCallout, remarkLayout, remarkAnnotate, remarkTabs, remarkStepper, remarkTimeline, remarkModal].
Selective registration (advanced)
If you want to support only some DSLs:
import { remarkCallout, remarkLayout } from '@rho/md';
processor
.use(remarkCallout)
.use(remarkLayout);
// Don't register others → tabs / stepper / timeline / modal / annotate not recognized
Note: with selective registration, you still need to call
registerAllInnerPlugins(), otherwise nesting even your registered plugins won't work. For strict isolation, useregisterInnerPlugin(plugin)to register individually.
Per-plugin details
remarkCallout
Trigger: blockquote first line matches [!TYPE].
Input:
> [!INFO]
> Body
> Multi-line.
Output:
<aside class="rho-callout rho-callout-info" data-type="INFO">
<header class="rho-callout-header">
<span class="rho-callout-icon">ℹ️</span>
<span class="rho-callout-title">INFO</span>
</header>
<div class="rho-callout-body">
<p>Body</p>
<p>Multi-line.</p>
</div>
</aside>
Supported types: INFO / WARN / ZEN (Rho native) + NOTE / TIP / IMPORTANT / WARNING / CAUTION (GFM). See Callout (writer).
Hydration: not required.
remarkLayout
Trigger: fenced block with lang layout grid cols=N.
Input:
\```layout grid cols=2
:::card accent=blue
A
:::
:::card accent=red
B
:::
\```
Output:
<div class="rho-layout-grid" data-cols="2">
<article class="rho-card rho-card-blue" data-accent="blue">
<p>A</p>
</article>
<article class="rho-card rho-card-red" data-accent="red">
<p>B</p>
</article>
</div>
Hydration: not required. CSS Grid + media query handles responsiveness.
remarkAnnotate
Trigger: fenced block with lang annotate.
Input:
\```annotate
The quick brown fox
---
4-9|red|adj
10-15|blue|noun
\```
Output:
<div class="rho-annotate">
The <span class="rho-anno rho-anno-red" data-label="adj" title="adj">quick</span>
<span class="rho-anno rho-anno-blue" data-label="noun" title="noun">brown</span> fox
</div>
Hydration: not required. Tooltip via CSS :hover + [title].
remarkTabs
Trigger: fenced block with lang tabs.
Input:
\```tabs default="B"
:::tab title=A
A content
:::
:::tab title=B
B content
:::
\```
Output:
<div class="rho-tabs" data-default="B">
<div class="rho-tabs-bar" role="tablist">
<button class="rho-tab-btn" data-tab-id="A">A</button>
<button class="rho-tab-btn rho-tab-active" data-tab-id="B">B</button>
</div>
<div class="rho-tab-panels">
<div class="rho-tab-panel" data-tab-id="A" hidden>A content</div>
<div class="rho-tab-panel" data-tab-id="B">B content</div>
</div>
</div>
Hydration: renderTabsBlocks(container) adds click handlers to buttons.
remarkStepper
Trigger: fenced block with lang stepper.
Input:
\```stepper linear=true
:::step title="A"
A
:::
:::step title="B"
B
:::
\```
Output:
<div class="rho-stepper" data-linear="true">
<div class="rho-stepper-progress" role="progressbar">
<span class="rho-step-marker rho-step-active" data-step="0">A</span>
<span class="rho-step-marker" data-step="1">B</span>
</div>
<div class="rho-step-panels">
<div class="rho-step-panel" data-step="0">A</div>
<div class="rho-step-panel" data-step="1" hidden>B</div>
</div>
<div class="rho-stepper-nav">
<button class="rho-step-prev" disabled>Prev</button>
<button class="rho-step-next">Next</button>
</div>
</div>
Hydration: renderStepperBlocks(container) adds Prev/Next click handlers and tracks active step.
remarkTimeline
Trigger: fenced block with lang timeline.
Input:
\```timeline
:::event date=2026-01-01 title="A"
desc A
:::
:::event date=2026-02-01 title="B"
desc B
:::
\```
Output:
<ol class="rho-timeline">
<li class="rho-event rho-event-blue" data-accent="blue">
<header class="rho-event-header">
<time class="rho-event-date">2026-01-01</time>
<h3 class="rho-event-title">A</h3>
</header>
<div class="rho-event-body">desc A</div>
</li>
<li class="rho-event rho-event-blue" data-accent="blue">
<header class="rho-event-header">
<time class="rho-event-date">2026-02-01</time>
<h3 class="rho-event-title">B</h3>
</header>
<div class="rho-event-body">desc B</div>
</li>
</ol>
Hydration: not required. CSS handles left-right alternating layout.
remarkModal
Trigger: fenced block with lang modal trigger="...".
Input:
\```modal trigger="show details" width=large
hidden content
\```
Output:
<a class="rho-modal-trigger" data-modal-id="m-abc123">show details</a>
<dialog class="rho-modal rho-modal-large" id="m-abc123" data-width="large">
<button class="rho-modal-close" aria-label="Close">✕</button>
<div class="rho-modal-body">hidden content</div>
</dialog>
Hydration: renderModalBlocks(container) wires trigger click → dialog.showModal(), close click → dialog.close().
Customizing / extending
Writing your own remark plugin?
@rho/md doesn't expose a plugin SDK — but since the base is standard unified, you can mix in your own remark plugins:
import myCustomPlugin from './my-custom-plugin';
processor
.use(remarkParse)
.use(remarkGfm);
remarkPlugins.forEach((p) => processor.use(p));
processor.use(myCustomPlugin); // Coexists with Rho
Suggestion: custom plugins should use different fenced lang labels to avoid conflicts (don't name them
interact/layout/ etc.).
Disable a specific plugin
// Skip remarkModal → modal blocks show as code-block literals
import { remarkCallout, remarkLayout, remarkTabs, remarkStepper, remarkTimeline, remarkAnnotate } from '@rho/md';
processor
.use(remarkCallout)
.use(remarkLayout)
.use(remarkTabs)
.use(remarkStepper)
.use(remarkTimeline)
.use(remarkAnnotate);
// Also: registerInnerPlugin only the corresponding 6, not remarkModal
See also
- Hydration utilities — runtime takes over the marker classes
- Inner processor — how plugins cascade with nested DSLs
- API reference
- CSS theming — restyle Rho's output HTML