Specification · Reference implementation · Conformance suite
Markdown for rich documents, without leaving Markdown
Markset adds a small, closed set of layout constructs to CommonMark: cards, grids, columns, tabs, steps, metrics, figures and callouts. Every valid CommonMark document is already a valid Markset document. Every Markset construct has a defined plain-CommonMark form it falls back to.
Release candidate v0.0.0-rc.2 CommonMark superset
Eight constructs is the whole vocabulary, and it is closed. Every one of them is pinned by cases in a shared test suite, so a second implementation can prove it agrees with this one rather than guessing; you can read every case, including the ones that are invalid on purpose, in the conformance browser. Nothing rendered from a Markset document contains a script, which is why tabs work by radio input and a folding callout is a <details> element.
Why it exists
Markdown has no attributes and no generic container
So rich documents reach for raw HTML, and that breaks portability, validation, and every output target that is not a browser. Pandoc, djot, Quarto, MyST, Markdoc and MDX each solved some of the syntax. None of them produced a set of components that independent renderers can agree on. That set is what Markset is.
Semantic, never presentational
Authors name what a thing is, not how it looks. Whether a card has a border is decided by the theme: a handful of named settings in the document's frontmatter, such as a preset, an accent color and a density. No inline CSS and no pixel values in source.
Every construct degrades
Each one wraps an ordinary CommonMark block and has a defined fallback. Paste a Markset file into a GitHub comment and it still reads, top to bottom, with nothing lost.
Closed vocabulary
There are eight constructs and there will not quietly be a ninth. An unknown directive is a reported error, not silent passthrough, so a document can be checked before it ships.
Nothing here executes. Interactivity is out of scope for the core specification, permanently. Tabs switch with radio inputs, callouts fold with <details>, and a document is data rather than code. That is what lets the same file render safely anywhere.
How it works
The same source, three ways
:::metrics
| Metric | Value | Δ |
|---------|-------|-------|
| Revenue | $4.2M | +12% |
| Churn | 2.1% | -0.4% |
:::
On the left is what you write: an ordinary Markdown table, wrapped in a fence that names what it is. On the right is the same source rendered by markset html, the command line tool in this repository, which turns it into metric tiles and reads the direction of each delta from its sign.
Run that source through markset downgrade instead and you get the table back, unchanged. Paste it into anything that has never heard of Markset and you get the table as well. The construct adds meaning without taking the content hostage.
Both commands, and the two others, are described on the CLI page.
Prior art
Reuse, don't invent
The syntax is the convergent one. Attribute specifiers {#id .class key=value}, fenced directives :::name and bracketed spans [text]{.class} already exist across Pandoc, djot, MyST and remark-directive, and callouts use GitHub's > [!NOTE] unchanged. Nothing here is a new spelling of an old idea. What is new is the closed set of constructs on top, and the rule that every one of them has a defined plain-CommonMark form.
| Project | Attributes | Generic container | Portable component vocabulary | Document stays inert |
|---|---|---|---|---|
| Pandoc | {#id .class} |
fenced divs | None | Yes |
| djot | native | native divs | None | Yes |
| Quarto | {.class} |
fenced divs | Product-specific | Executes code |
| MyST | directives | directives | Open and extensible | Executes code |
| Markdoc | typed tags | typed tags | Defined per project | Yes |
| MDX | JSX props | JSX | Your components | Executes code |
| Markset | {#id .class} |
:::name |
Closed and portable | Yes |
"Portable" means another implementation can render the same document from the specification alone. "Inert" means nothing in a document is evaluated in order to render it.
Start here
Three ways in
- Read the reference for one page per construct, each with live examples pulled straight from the test suite.
- Read the specification. It is short, and it is the source of truth: when the code and the spec disagree, the spec wins.
- Read the CLI page if you would rather start by running something.
Try it locally
git clone https://github.com/markset-lang/markset && cd markset
npm install
node packages/cli/src/markset.ts html examples/showcase.md -o showcase.html
See it at length
Three complete documents, not fragments.
The construct tour uses every one of the eight constructs exactly once, on the default stylesheet, so you can see the whole vocabulary at its real size.
The analysis document and the strategy memo are long documents of the kind Markset is actually for. Each adds a theme stylesheet of its own, and the difference between the two shows how far appearance can move while the source stays the same shape.