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 ```interact blocks are converted by unified default to <pre><code>, then renderInteractBlocks recognizes by lang label and takes over.


All-vs-individual registration

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, use registerInnerPlugin(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