Herman Stander
Core team developer and marketing
2025-08-25
When your Markdown becomes HTML, how do you keep it interactive?
Let’s add a Copy to clipboard button to every code block in a RedwoodSDK blog without MDX or heavy remark plugins.
Rendering Markdown in RedwoodSDK is simple: turn it into HTML and drop it into the page. But:
dangerouslySetInnerHTML
.Classic example: copy-to-clipboard buttons on code blocks.
Browsers natively support Custom Elements. That means:
<copy-button>
right in your HTML.customElements.define(...)
on the client.<copy-button>
tags into live components with logic.This makes Web Components a perfect match for Markdown:
they can be embedded in static HTML, yet still work interactively.
Here’s a RedwoodSDK client component that highlights code with highlight.js
:
"use client";
import { useEffect, useRef } from "react";
import hljs from "highlight.js";
export default function Content({ content }: { content: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
ref.current.querySelectorAll("pre code").forEach((block) => {
hljs.highlightElement(block as HTMLElement);
});
}, [content]);
return (
<div
ref={ref}
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: content }}
/>
);
}
<copy-button>
Inside the same component, register the custom element on the client:
useEffect(() => {
if (typeof window !== "undefined" && !customElements.get("copy-button")) {
class CopyButtonEl extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const btn = document.createElement("button");
btn.textContent = "Copy";
btn.onclick = async () => {
const pre = this.parentElement;
const code = pre?.querySelector("code")?.textContent ?? "";
await navigator.clipboard.writeText(code);
btn.textContent = "Copied!";
setTimeout(() => (btn.textContent = "Copy"), 1200);
};
shadow.append(btn);
}
}
customElements.define("copy-button", CopyButtonEl);
}
}, []);
Still inside the effect, loop through <pre>
tags and append:
const pres = ref.current.querySelectorAll("pre");
pres.forEach((pre) => {
if (!pre.querySelector("copy-button")) {
pre.style.position = "relative";
pre.appendChild(document.createElement("copy-button"));
}
});
Now every <pre>
gets its own button.
<copy-button>
. Browser upgrades it. This trick applies to more than copy buttons: think tabs, spoilers, tooltips, callouts, etc.
Web Components are the simplest way to sprinkle interactivity into Markdown-rendered content.
With RedwoodSDK’s RSC/SSR model, you get the best of both worlds:
Happy building!