Web-component to load an external markdown file (.md) and render it into sanitized HTML.
- GFM (GitHub Flavored Markdown spec)
- Light DOM CSS styling
- Optional JavaScript API
- Syntax highlighting
- Table of contents
- Copy code to clipboard
- Media embedding (image, audio, video)
📦 Scoped @xan105 packages are for my own personal use but feel free to use them.
🤔 Curious to see it in real use? This package powers my personal blog.
Import and define the Web-component:
import { Markdown } from "/path/to/markdown.js"
customElements.define("mark-down", Markdown);HTML:
<mark-down src="/path/to/md"></mark-down>Optional JavaScript API:
const el = document.querySelector("mark-down");
el.addEventListener("load", ()=>{
console.log("loading...");
});
el.addEventListener("success", ()=>{
console.log("ok");
});
el.addEventListener("failure", ({detail})=>{
console.error(detail.error);
});
//auto rendering (default)
el.integrity = "sha384-0xABCD...";
el.src = "/path/to/md";
//manual rendering
el.manual = true;
el.src = "/path/to/md";
el.render().catch((err)=>{
console.error(err);
});
//Table of contents
const toc = el.headings.createElement({ depth: 4 });
document.querySelector("#toc").replaceWith(toc);
el.addEventListener("intersect", ({detail})=>{
//Do something when a heading (h1, h2, ...) has entered the top of the viewport
document.querySelector(`#toc a[href="#${detail.id}"]`).classList.add("active");
});npm i @xan105/markdown
💡 The bundled library and its minified version can be found in the ./dist folder.
Create an importmap and add it to your html:
<script type="importmap">
{
"imports": {
"@xan105/markdown": "./path/to/node_modules/@xan105/markdown/dist/markdown.min.js"
}
}
</script>
<script type="module">
import { Markdown } from "@xan105/markdown"
customElements.define("mark-down", Markdown);
</script>
</body>
</html>Markdown is rendered into the light DOM without any predefined CSS styling, this is by design.
Use regular selectors to style just like you would for the rest of the page.
For syntax highlighting you can use one of the many hljs themes available.
💡That being said, there is a basic CSS style with Github-like syntax highlighting available in the ./dist folder to get you started.
To target the "copy to clipboard" unstyled button added to "code blocks" use CSS ::part() selector:
clipboard-copy-code { display: block } //by default it is not rendered (display: none)
clipboard-copy-code::part(button) { ... }
clipboard-copy-code::part(button)::before { /*go nuts this also works*/ }
clipboard-copy-code will have the attribute copied set when the content has been copied to the clipboard;
You can target it via CSS and add a timeout (ms) attribute/property value if you wish to do some kind of animation on copy.
clipboard-copy-code also fires a copied event just in case.
The markdown image syntax has been extended to support audio and video in addition to image.
Media are represented inside a <figure> with an optional <figcaption> and rendered with their corresponding html tag.




-
url: The URL of the media file. Can be an image, audio, or video file. -
text(optional): The text caption (also used as thealttext for images). -
@size(optional): Size override in pixels aswidthxheight.For advanced sizing requirements, consider using CSS instead.
-
mime(optional): The MIME type of the file (e.g., image/png, audio/ogg; codecs=opus, video/mp4).If the MIME type is omitted, this library will try to infer it from the file extension. If the file extension is ambiguous (e.g., .mp4, .webm, .ogg), it performs a
HEADrequest to fetch theContent-Typefrom the server.The "mime" attribute is mainly for audio/video containers, providing it:
- avoid extra network request for MIME detection.
- ensure correct codec/container handling for audio/video.
Example
Renders as:
<figure>
<video controls preload="metadata" width="640px" height="480px">
<source src="./mov_bbb.mp4" type="video/mp4">
</video>
<figcaption>Big Buck Bunny</figcaption>
</figure>For more advanced media type (e.g., canvas, iframe, web-component) you should use the html "as is" within the markdown file, and if necesarry, allow it in the sanitizeOptions of the render() method (see below).
Example
I personally do this for my STL renderer: xan105/web-component-3DViewer (experimental).
const md = document.querySelector("mark-down");
await md.render({
CUSTOM_ELEMENT_HANDLING: {
tagNameCheck: /^stl-viewer$/,
attributeNameCheck: (attr) => ["src", "gizmos", "pan", "zoom", "rotate", "inertia"].includes(attr),
allowCustomizedBuiltInElements: false
}
});
//Conditional Import
if (document.querySelector("stl-viewer")) {
const { STLViewer } = await import("@xan105/3dviewer");
customElements.define("stl-viewer", STLViewer);
await customElements.whenDefined("stl-viewer")
}This is a Web-component as such you need to define it:
import { Markdown } from "/path/to/markdown.js"
customElements.define("mark-down", Markdown);Events
-
change()The source (src) attribute has changed.
-
load()Markdown is being loaded.
-
render()Markdown is being rendered.
-
success()Markdown was rendered without any issue.
-
failure(detail: object)Something went wrong, see
detail:{ error: Error }
-
intersect(detail: object)A heading (h1, h2, ...) has entered the top of the viewport, see
detail:{ id: string }
Attribute / Property
-
src: stringPath/URL to the
.mdfile to load. -
integrity: stringIntegrity hash passed to
fetch(). See Subresource Integrity for more details. -
manual: booleanIf set markdown will not be rendered automatically and you will have to call the
render()method yourself (see below). -
rendered: boolean(Read-only)Whether the markdown was succesfuly rendered or not. You can use
:not([rendered])in your CSS to style the element differently before rendering.
Property
-
headings: Set<object>(Read-only)List of all headings (h1, h2, ...) with an id and text content represented as follows:
{ id: string, level: number, title: string }
Example:
//<h2 id="user-content-links">Links</h2> { id: "user-content-links", title: "Links", level: 2 }
The returned
Setis extended with additional functions:createElement()andtoHTML():-
createElement(options?: object): HTMLElementReturns a HTMLElement representing the table of contents from the headings (nested list).
Options:
-
ordered?: boolean(false)Whether to use
ul(false) orol(true) as HTMLElement. -
depth?: number(6)How deep to list ? Headings start from 1 to 6.
-
-
toHTML(options?: object): stringSame as above but returns the list as a raw HTML string, eg:
<ul> <li><a href="#id">title</a></li> <li> <ul> <li><a href="#id">title</a></li> <li><a href="#id">title</a></li> </ul> </li> <ul/>
-
Methods
-
render(sanitizeOptions?: object): Promise<void>Load and render markdown into sanitized HTML.
👷🔧 You can pass an optional DOMPurify configuration object to configure the sanitization.
✔️ Resolves when markdown has been sucesfully rendered.
❌ Rejects on error💡 Invoking this method still triggers related events.
-
estimateReadingTime(speed?: number): numberEstimate the "time to read" of the markdown's content in minutes.
By defaultspeedis265words per minute; the average reading speed of an adult (English).
