# zero-md: dropping a markdown file into a page with one custom element

> zero-md is a web component that fetches a markdown file and renders it inside a Shadow DOM container, with marked for parsing, highlight.js for code, KaTeX for math and Mermaid for diagrams. It removes the build step from the equation, and it also removes any chance of styling the output from the page.

**zerodevx/zero-md** — Ridiculously simple zero-config markdown displayer

- Repository: https://github.com/zerodevx/zero-md
- Website: https://zerodevx.github.io/zero-md/
- Stars: 558 · Forks: 53
- Language: JavaScript
- License: ISC
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/zerodevx-zero-md

## One element, one attribute, no build step

The pitch is a single custom element. You put a zero-md tag in your page, point its src attribute at a markdown file, and the browser fetches that file and renders it. There is no build step, no bundler configuration and no template engine in the middle.

The element is built on the Custom Elements V1 specification and renders inside a self-contained Shadow DOM container. Under the hood it uses marked for parsing and highlight.js for syntax highlighting, and the topic tags reflect that stack: markdown-to-html, markedjs, highlightjs, katex, mermaid and webcomponents.

The shortest possible usage is the CDN form.

```html
<head>
  <!-- Import element definition and auto-register -->
  <script type="module" src="https://cdn.jsdelivr.net/npm/zero-md@3?register"></script>
</head>
<body>
  <!-- Profit! -->
  <zero-md src="example.md"></zero-md>
</body>
```

The ?register query parameter is the small trick that makes this work in one line. It auto-registers the custom element, so you skip the import and the customElements.define call. The documentation says the above is semantically equivalent to loading the module yourself and registering it by hand, which is the form to use when you want control over the registration.

For a project that already has a JavaScript build, the package installs from npm and the element is imported and registered like any other module.

```js
// Import the element definition
import ZeroMd from 'zero-md'

// Register the custom element
customElements.define('zero-md', ZeroMd)

// Render anywhere
app.render(`<zero-md src=${src}></zero-md>`, target)
```

The last line is the important one for application developers. Because the element is a real custom element, you can render it through whatever templating your application uses, which is what makes it usable inside a single page app rather than only in a static page.

## The Shadow DOM boundary decides what you can style

The single most consequential design choice in zero-md is the Shadow DOM container, and it cuts in two directions.

What it buys you is isolation. The rendered markdown lives in its own tree with its own styles, so the page's stylesheet cannot accidentally restyle a code block or a table inside the component, and the component's styles cannot leak out and hit the surrounding page. For a component that is dropped into an unknown page, that is a reasonable default.

What it costs you is control. The documentation includes a mechanism for adding your own styles while keeping the defaults, and it works by placing a template element with a data-append attribute inside the component.

```html
<!-- Add custom styles while keeping defaults -->
<zero-md src="https://example.com/markdown.md">
  <template data-append>
    <style>
      p { color: red; }
    </style>
  </template>
</zero-md>
```

That is the escape hatch, and it is narrow by design. You append CSS into the component's own tree rather than reaching in from the page, and the style element is scoped to the component. If your site needs to restyle headings to match a brand, or to set a font stack across the rendered content, you are writing into the component, not styling it from outside.

The component also ships light and dark themes out of the box, which handles the most common case of a documentation page appearing on a site with a dark mode. The list of features also includes FOUC prevention, which is a flash of unstyled content problem that occurs when content is injected into the page after it has already painted. Handling that internally is a good sign about how the component thinks about rendering.

Auto re-render when inputs or attributes change is the other feature worth knowing, since it means a change to the src attribute re-fetches and re-renders rather than leaving stale content on the page.

## Math, diagrams and code highlighting come along for the ride

The feature list covers three kinds of rich content that a plain markdown to HTML conversion would not handle on its own.

Math rendering is done with KaTeX, and diagrams with Mermaid. Both are the community standard tools for their jobs, and both are pulled in by the component rather than left to you, which is the whole point of the zero-config approach. Syntax highlighting uses highlight.js with auto-language detection, so fenced code blocks do not need a language tag to be highlighted.

The rest of the list is more infrastructural. Hash-link scroll handling for anchor navigation matters for documentation, where a table of contents links to headings and the browser has to scroll to the right place after the markdown has been fetched and rendered. That is a timing problem rather than a styling one, and the component handles it.

Spec-compliant extensibility is listed as a feature, which is a slightly odd thing to advertise for a component that deliberately closes itself off from outside styling. Read in context, it refers to the Custom Elements V1 lifecycle rather than to a plugin API. There is no documented list of extension points, and the documentation is the place to look for the details.

One more element of the design is worth stating plainly. The component fetches its markdown over the network at runtime, which means the content is not in the page at request time. For a site that needs the text in the initial HTML for search indexing or for readers with JavaScript disabled, that is a different trade from a build-time markdown pipeline. zero-md is a display layer, and the display happens in the browser.

## Local files are out of reach, and that is a browser rule

There is one constraint the documentation puts in an important callout rather than burying, and it is the one most likely to trip up a first attempt.

Markdown files must be served over HTTP or HTTPS. Browsers restrict local file access via the file protocol because of security policies, and standard CORS rules apply on top of that. If you clone the project, open the HTML file directly from disk, and point zero-md at a markdown file sitting next to it, nothing will render.

This is a browser policy rather than a limitation in the component, but it still shapes the first hour of use. The fix is to serve the directory with any static server, and the development workflow does exactly that.

The setup for working on the library itself is a normal clone and install.

```bash
git clone https://github.com/zerodevx/zero-md.git
cd zero-md
npm i
```

Then the dev server starts with a single command.

```bash
npm run dev
```

The dev server is powered by Vite, and the test page is at http://localhost:5173, with the library source files under src/lib. The test suite runs on Playwright with npm test, and the specifications live in src/index.spec.js.

The same constraint applies in production. If your site is served from a CDN, the markdown file has to be fetchable by the browser with a CORS header that allows it, which usually means the same origin as the page or an explicit Access-Control-Allow-Origin. A markdown file on a different domain without that header will fail in the same way the file protocol fails.

## Browser support is a hard floor at Custom Elements and Shadow DOM

The documentation states the requirement plainly: a modern evergreen browser with Shadow DOM and Custom Elements v1 support, and no IE11 support. That is a factual statement rather than a hedge, and it should be read as a hard floor for any deployment.

The reason is structural. Registering a custom element and attaching a shadow root are the two primitives the component is built on, and there is no polyfill path in the package. There is no fallback content in the markup, and no server-side rendering story, so the markdown simply does not appear in a browser that lacks either primitive.

There is a related point for search engines and for text extraction tools. Because the content is fetched and rendered client side, a crawler that does not execute JavaScript will see the page without the markdown body. The documentation does not address this, and it is the kind of question that matters more for a documentation site than for a personal scratchpad.

The package manifest also shows the build in enough detail to understand what ships. The build script runs vite build, then a terser step to produce dist/index.min.js, then a tsc step that emits type declarations into dist/types. The exports map exposes the main entry with its types and import paths, and a secondary path that exposes individual source files from src/lib. The jsdelivr field points at dist/index.min.js, which is what the CDN loads.

For anyone who wants a single file with no dependencies to resolve at runtime, the documentation points at a separate project, zero-md-bundled, described as a self-contained pre-bundled package.

## v3 is a rewrite, and the v2 branch is still there

The version numbers in the repository deserve a mention, because the project is not a drop-in replacement for the earlier line.

The documentation carries a note stating that this is the v3 branch, that the older version lives on a v2 branch, and that anyone upgrading from v2 should read the migration guide in docs/migration.md. The v3 line is what the npm package and the CDN point at, with the current version at 3.1.9, following 3.1.8 in July 2026 and 3.1.7 in January 2025.

The cadence in that list is worth reading carefully rather than characterising. There were several months between the January 2025 release and the July 2026 one, and the last push was on 2026-10-08, days before the v3.1.9 tag. So the project is being worked on, but the release history is not a fast one, and there is no statement in the documentation about a support window for a given major version.

The repository structure supports a small team rather than a large one. Alongside the source there is a docs folder, an eslint config, a jsconfig, a prettier config, a playwright config, an outdated.mjs script used by the check task, and a package-lock. The check script runs outdated.mjs, which is a dependency freshness check, and the package script runs check, then build, then publint, so a published package is linted for packaging correctness.

The documentation workflow is documented too. Docs source lives in the docs folder, pull requests edit the markdown files directly, and the documentation website uses a separate zero-md-docs repository to publish changes from that folder after a merge. In other words, the project eats its own cooking: the documentation site that describes zero-md is built with zero-md.

The licence is ISC, which is short and permissive, and the package manifest confirms it in the license field with the repository and homepage fields filled in. There is a LICENSE file in the root, and nothing in the visible documentation imposes additional terms on use.

## Where a browser side markdown renderer stops being the right tool

The case against zero-md is about where rendering happens, not about what it does.

The first problem is content availability. Because the markdown is fetched at runtime, the rendered text is absent from the initial HTML. Any requirement that the content be in the response, whether for indexing, for a client that does not run JavaScript, or for a link preview, is not met by this approach. A build-time markdown pipeline, the kind Vite plugins and static site generators provide, produces the finished page on the server and has no such gap.

The second is the Shadow DOM styling boundary, which is a feature from the component's point of view and a constraint from yours. If a design system needs to control typography, link colours and heading scale inside rendered documentation, you are appending styles into the component rather than styling it from the page, and the mechanism is limited to what the data-append template exposes. A build-time approach puts the rendered HTML in the light DOM, where the site's own CSS applies normally.

The third is the browser floor. Custom Elements and Shadow DOM are supported in current browsers, and that is what you should assume for internal tools. But a site with older or restricted clients, an embedded webview, or a text-mode reader, sees nothing, and there is no progressive fallback in the package.

The alternative to weigh against it is a build-time markdown tool, such as a Vite plugin or a static site generator plugin, which converts markdown to HTML during the build so the page ships complete markup. The difference in approach is precisely where the work happens. A build-time tool spends compile time and gives you full control over the output DOM and the CSS that applies to it. zero-md spends nothing up front, needs no toolchain, and gives you an isolated component with a narrow style hook. For a documentation page on a small site, the second is usually the better deal. For a public site where the content has to be indexable and themeable, the first is.

One practical note on cost: there is no build step, but there is a runtime dependency on the CDN serving the module, and the component fetches the markdown on every page that uses it. Whether that matters depends on your traffic and your caching headers.

## Conclusion

zero-md suits a documentation page, a README viewer or a small site that wants readable markdown with no build tooling, and it does not suit a site that needs to theme the rendered output from outside the component, because the Shadow DOM boundary deliberately blocks that. Before you adopt it, confirm that your markdown is served over HTTP rather than opened from a file path, since the component cannot read local files, and check that the browser support policy of your users covers Custom Elements and Shadow DOM, because there is no IE11 fallback.

## FAQ

### What is zero-md and how do I use it?

zero-md is a zero-config web component that converts markdown to HTML inside a Shadow DOM container. You add a zero-md element with a src attribute pointing at a markdown file, or write markdown inline in a script tag with type text/markdown.

### Why does my markdown not render when I open the HTML file directly?

Markdown files must be served over HTTP or HTTPS. Browsers restrict local file access via the file protocol because of security policies, and standard CORS rules apply, so you need to serve the directory with a static server.

### How do I load zero-md from a CDN?

Import the script from jsdelivr with the ?register query param, which imports the element definition and auto-registers it, then add a zero-md element with a src attribute. The documentation also shows the equivalent form that imports the module and calls customElements.define yourself.

### Can I style the markdown that zero-md renders?

You can append your own styles inside the component with a template element carrying a data-append attribute. Because rendering happens inside a Shadow DOM container, the page's own stylesheet cannot restyle the rendered content from outside.

### What browsers does zero-md support?

It requires a modern evergreen browser with Shadow DOM and Custom Elements v1 support, and there is no IE11 support. The component is built on those two primitives with no fallback content.

## Sources

- [License: ISC](https://github.com/zerodevx/zero-md/blob/main/LICENSE)
- [Project website](https://zerodevx.github.io/zero-md/)
- [README](https://github.com/zerodevx/zero-md/blob/main/README.md)
- [Releases](https://github.com/zerodevx/zero-md/releases)
- [zerodevx/zero-md on GitHub](https://github.com/zerodevx/zero-md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/zerodevx-zero-md
