--- imports: - ./dist/walking-dinosaur.js - https://cdn.jsdelivr.net/npm/@opral/markdown-wc-doc-elements/dist/doc-video.js ---
npm install @opral/markdown-wc---
imports:
- ./dist/walking-dinosaur.js
- https://cdn.jsdelivr.net/npm/@opral/markdown-wc-doc-elements/dist/doc-video.js
---
Enables writing documentation with web components in markdown.
``markdown
---
imports:
- https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/walking-dinosaur.js
---
Enables writing documentation with web components in markdown.
`
- Enables writing documentation with components in markdown
- Interoperable with existing markdown parsers
- Doesn't depend on a framework like React MDX or Svelte MDsveX
- Doesn't introduce custom syntax like Markdoc
| Feature | Markdown | @opral/markdown-wc | React MDX | Svelte MDsveX | Markdoc |
| ---------------------- | -------- | ------------------ | --------- | ------------- | ------- |
| Components in markdown | ❌ | ✅ | ✅ | ✅ | ✅ |
| Framework agnostic | ✅ | ✅ | ❌ | ❌ | ✅ |
| Portable | ✅ | ✅ | ❌ | ❌ | ❌ |
| No custom syntax | ✅ | ✅ | ❌ | ❌ | ❌ |
Portable: the output is standard HTML + web components without framework-specific runtime requirements.
Install:
`bash`
npm install @opral/markdown-wc
Server-side (or build-time) parse:
`ts
import { parse } from "@opral/markdown-wc"
const markdown =
const { html, frontmatter } = await parse(markdown)
`Client-side: register custom elements listed in
frontmatter.imports:`ts
for (const url of frontmatter.imports ?? []) {
await import(url)
}
`Then render
html inside a container (see Styling below).Usage in browser
The
element can be used to embed markdown-wc in a webpage.
It renders in the light DOM so the host page's CSS applies to embedded markdown by default.`html
type="module"
src="https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/markdown-wc-embed.js"
>
`Usage in another markdown file
The
element can be used to embed markdown-wc in markdown-wc.
imports must be a list of module URLs (array syntax). Wrap the embed in
.markdown-wc-body if you want the default stylesheet to apply.`markdown
---
imports:
- https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/markdown-wc-embed.js
---Hello World
This is a markdown file that embeds another markdown file 🤯
`Usage as libary
Enables SSR and more control over the rendering process.
`ts
import { parse } from "@opral/markdown-wc"const markdown =
// This is a web component Card content
Card Title
// Parse markdown
const parsed = parse(markdown)
// Optionally open external links in new tabs
// const parsed = parse(markdown, { externalLinks: true })
// Resolve relative asset URLs (images/links) against a base path
// Markdown: !Architecture
// Result: /blog/my-post/assets/architecture.jpg
// const parsed = parse(markdown, { assetBaseUrl: "/blog/my-post/" })
// Register web components
for (const url of parsed.frontmatter.imports ?? []) {
// optionally sanitize the imported imported here
// by, for example, only trusting a specific domain
await import(url)
}
// render HTML
render(parsed.html)
`
Markdown WC does not sanitize HTML output. If you render untrusted or remote
markdown, you must sanitize or trust the source before rendering.
Markdown WC renders standard HTML elements (headings, paragraphs, lists, blockquotes,
code blocks, tables, etc.) and does not inject inline styles. This keeps the output
portable and allows the host application to fully control styling.
Code blocks are syntax-highlighted, but no Highlight.js stylesheet is injected.
To enable highlighting styles, load a Highlight.js theme in your app (or provide your own .hljs styles).
Example:
`html`
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css"
/>
React example:
`tsx`
export function MarkdownStyles() {
return (
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css"
/>
);
}
To get the default look, import the bundled stylesheet and wrap your rendered HTML
with a .markdown-wc-body container (recommended to avoid CSS collisions). This
is also the expected styling model for .
`css`
@import "@opral/markdown-wc/default.css";
Or provide your own CSS. Example:
`css
/ Wrap rendered HTML in a container and style inside it /
.markdown-wc-body {
color: #213547;
font-size: 16px;
line-height: 1.7;
}
.markdown-wc-body h1 {
font-size: 2rem;
font-weight: 600;
margin: 0 0 1rem;
}
.markdown-wc-body h2 {
font-size: 1.5rem;
font-weight: 600;
margin: 2rem 0 0.75rem;
border-top: 1px solid #e2e2e3;
padding-top: 1.25rem;
}
.markdown-wc-body p {
margin: 1rem 0;
}
.markdown-wc-body a {
color: #3a5ccc;
text-decoration: underline;
text-underline-offset: 3px;
}
`
Alert styling example (GitHub alerts):
`css
.markdown-wc-body blockquote[data-mwc-alert] {
border-left: none;
border-radius: 8px;
padding: 12px 16px;
margin: 1rem 0;
}
.markdown-wc-body blockquote[data-mwc-alert] [data-mwc-alert-marker] {
display: none;
}
.markdown-wc-body blockquote[data-mwc-alert="note"] {
background: rgba(100, 108, 255, 0.08);
border: 1px solid rgba(100, 108, 255, 0.16);
}
.markdown-wc-body blockquote[data-mwc-alert="note"]::before {
content: "Note";
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
color: #3451b2;
}
`
Code block styling / copy button hook example:
`css
.markdown-wc-body pre[data-mwc-codeblock] {
position: relative;
padding: 16px 20px;
border-radius: 8px;
background: #f6f6f7;
overflow-x: auto;
}
.markdown-wc-body pre[data-mwc-codeblock] > button[data-mwc-copy-button] {
position: absolute;
top: 8px;
right: 8px;
font-size: 12px;
padding: 4px 8px;
border-radius: 6px;
border: 1px solid #e2e2e3;
background: #fff;
opacity: 0;
}
.markdown-wc-body pre[data-mwc-codeblock]:hover > button[data-mwc-copy-button] {
opacity: 1;
}
`
- sanitzation of markdown as well as custom elements is not implemented atm
- SSR is DIY atm (use the parse function and SSR the markdown with lit for example)
Markdown WC recognizes GitHub-style alert syntax inside blockquotes:
`md
> [!NOTE]
> Highlights information that users should take into account.
> [!TIP]
> A helpful tip.
> [!IMPORTANT]
> Crucial information necessary for users to succeed.
> [!WARNING]
> Critical content demanding immediate attention.
> [!CAUTION]
> Dangerous or destructive actions.
`
Alerts are emitted as normal blockquotes annotated with data attributes:
`html`
[!NOTE]
Highlights information that users should take into account.
Supported alert types: note, tip, important, warning, caution.
Markdown WC does not ship alert CSS. Style them in your site using the data attributes.
Example (VitePress-like):
`css
blockquote[data-mwc-alert] {
border-left: none;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
blockquote[data-mwc-alert] [data-mwc-alert-marker] {
display: none;
}
blockquote[data-mwc-alert="note"] {
background: rgba(100, 108, 255, 0.08);
border: 1px solid rgba(100, 108, 255, 0.16);
}
blockquote[data-mwc-alert="note"]::before {
content: "Note";
font-weight: 600;
color: #3451b2;
display: block;
margin-bottom: 8px;
}
`
Markdown WC keeps code blocks portable by rendering standard HTML:
`html`
...
The data-mwc-codeblock attribute is a stable hook for consumer styling and
client-side hydration such as adding a “Copy” button. Markdown WC intentionally
does not render custom UI elements for code blocks; consumers should implement
copy buttons and other enhancements with CSS and client-side scripts to keep the
renderer framework-agnostic and portable.
Example copy-button script (based on inlang website-v2). This targets code blocks
inside .markdown-wc-body and appends a button.mwc-copy-button:
`ts
const COPY_BUTTON_CLASS = "mwc-copy-button"
function ensureCopyButtons(root: Document | Element = document) {
const blocks = root.querySelectorAll(".markdown-wc-body pre:has(> code)")
for (const pre of blocks) {
if (pre.querySelector(.${COPY_BUTTON_CLASS})) continue
const button = document.createElement("button")
button.type = "button"
button.className = COPY_BUTTON_CLASS
button.textContent = "Copy"
;(pre as HTMLElement).style.position = "relative"
pre.appendChild(button)
}
}
function handleCopyClick(event: Event) {
const target = event.target
if (!(target instanceof HTMLElement)) return
const button = target.closest(.${COPY_BUTTON_CLASS})
if (!button) return
const pre = button.closest("pre")
const code = pre?.querySelector("code")?.textContent ?? ""
navigator.clipboard.writeText(code)
const previous = button.textContent
button.textContent = "Copied!"
window.setTimeout(() => {
button.textContent = previous || "Copy"
}, 1500)
}
export function initMarkdownCopyButtons() {
if (typeof window === "undefined") return
ensureCopyButtons()
document.addEventListener("click", handleCopyClick)
const observer = new MutationObserver(() => ensureCopyButtons())
observer.observe(document.body, { childList: true, subtree: true })
}
`
Markdown WC supports Mermaid fenced code blocks:
`md`mermaid`
graph TD
A --> B`
When Mermaid blocks are detected, Markdown WC emits a element and adds an import URL to frontmatter:
`ts`
const { html, frontmatter } = await parse(markdown)
// frontmatter.imports includes:
// "https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/markdown-wc-mermaid.js"
Consumers must load frontmatter.imports on the client so the custom element is registered before render.
React MDX and Svelte MDsveX are great tools but they introduce a dependency on a specific framework which is a no-go for portability.
Markdown parsers don't remove the