Interactive Markdown
Plain .md files can host real interactivity: a <script> tag written
directly in the Markdown is shipped verbatim and runs in the browser. This
page is a single .md file — open its source to see there are no
components anywhere. Every demo below is just HTML + a bit of inline JS,
and every demo shows the source you would write in your own docs.
This is the recommended way to add interaction to docs and posts synced from external repositories: write the HTML +
<script>directly in your Markdown. No hub-side code, no shared library — your content is self-contained, and the hub’s PR review gates what ships.
Buttons
Click a button, watch a value change:
Counter — the classic. Click the buttons.
The source — a button with an onclick, plus a small script that mutates a
<span>:
<button class="btn btn-secondary" onclick="myCounter('plus')">+</button>
<span id="my-counter">0</span>
<script>
var n = 0;
function myCounter(action) {
if (action === 'plus') n++;
document.getElementById('my-counter').textContent = String(n);
}
</script>
Tabs
Group content behind tabs — switch panels by clicking:
Package managers — click a tab.
npm install my-package
pnpm add my-package
yarn add my-package
The pattern — a tab bar plus panels, toggled by class:
<div class="demo-tabs">
<button class="demo-tab is-active" onclick="demoTabs('npm')">npm</button>
<button class="demo-tab" onclick="demoTabs('pnpm')">pnpm</button>
</div>
<div class="demo-panel is-active" data-panel="npm">…</div>
<div class="demo-panel" data-panel="pnpm">…</div>
<script>
function demoTabs(name) {
document.querySelectorAll('.demo-tab').forEach(function (t) {
t.classList.toggle('is-active', t.getAttribute('data-tab') === name);
});
document.querySelectorAll('.demo-panel').forEach(function (p) {
p.classList.toggle('is-active', p.getAttribute('data-panel') === name);
});
}
</script>
Icons
Inline SVG — no icon library. Static icons render as-is; a button can swap between two icons:
Toggle icon — click to flip between sun and moon.
Static icons (inline SVG, just rendered).
Chart from a JS function
Write a function that generates data, draw it as an SVG — no chart library. Click Roll to regenerate (same seed → same shape), or switch line/bar:
Random walk — a JS function produces the points.
The source — the generator is a plain function you can read:
<svg id="demo-chart" class="demo-chart" viewBox="0 0 600 220"></svg>
<button onclick="demoChartRoll()">Roll</button>
<script>
// A deterministic PRNG so the same seed draws the same chart.
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6d2b79f5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
var demoChartSeed = 7;
var demoChartAsBar = false;
function demoChartData() {
var r = mulberry32(demoChartSeed);
var v = 50, out = [];
for (var i = 0; i < 24; i++) {
v = Math.max(5, Math.min(95, v + (r() - 0.5) * 24));
out.push(v);
}
return out;
}
function demoChartDraw() {
var svg = document.getElementById('demo-chart');
var values = demoChartData();
var asBar = document.getElementById('demo-chart-bar').checked;
var W = 600, H = 220, PAD = 8;
var x = function (i) { return PAD + (i / 23) * (W - PAD * 2); };
var y = function (v) { return H - PAD - (v / 100) * (H - PAD * 2); };
var inner = '';
if (!asBar) {
var pts = values.map(function (p, i) {
return x(i).toFixed(1) + ',' + y(p).toFixed(1);
}).join(' ');
inner += '<polyline points="' + pts + '" fill="none" stroke="var(--color-accent)" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>';
inner += values.map(function (p, i) {
return '<circle cx="' + x(i).toFixed(1) + '" cy="' + y(p).toFixed(1) + '" r="2.5" fill="var(--color-accent)"/>';
}).join('');
} else {
var bw = ((W - PAD * 2) / 24) * 0.7;
inner = values.map(function (p, i) {
var hh = Math.max(2, H - PAD - y(p));
return '<rect x="' + (x(i) - bw / 2).toFixed(1) + '" y="' + (H - PAD - hh).toFixed(1) +
'" width="' + bw.toFixed(1) + '" height="' + hh.toFixed(1) + '" rx="1.5" fill="var(--color-accent)"/>';
}).join('');
}
svg.innerHTML = inner;
document.getElementById('demo-chart-count').textContent = values.length + ' points';
}
function demoChartRoll() { demoChartSeed = Math.floor(Math.random() * 1e9); demoChartDraw(); }
// Draw on load.
demoChartDraw();
</script>
Best practices
- Keep it self-contained. All styles and scripts live in this one
.mdfile. Use ademo-(or product-specific) prefix on classes andids so your page never collides with the hub’s global styles or another page. - No external requests. Interactivity here is pure DOM + math. Avoid
fetch/XMLHttpRequest,eval, and cookie access in content — the hub’s review gates this, and it keeps your page fast and safe. - One
<script>at the end defining all functions is easier to read than scattered inline handlers, and keeps the page organized. - Reuse the hub’s CSS classes (
btn,btn-primary,btn-secondary) so your buttons match the site; add a small<style>block only for your custom bits (cards, tabs, chart).