Introducing Supermark: a markup language for AI interfaces
Like most devs today, I now build software using AI-first workflows, so I spend a good part of every day having conversations with Claude and reading what it writes. That second part is the whole reason Supermark exists. It isn't about building apps. It's about reading a conversation.
Want to see it in action? Jump straight to the playground →
In an AI conversation (and for documentation), Markdown is the status quo, and for good reason: it's cheap to generate, it streams cleanly, and you can read the raw text with no renderer at all. It's also a language with a long habit of being extended when people want more. GitHub wanted more than base Markdown offered, so GitHub-Flavored Markdown added tables, task lists, strikethrough, and bare-URL autolinks, and the extensions were useful enough that GFM became the default dialect; models emit it without being asked. GitHub kept going with alerts, which gave Markdown a callout syntax. Mermaid solved diagrams with a small DSL inside a code fence, and enough renderers support it now that it's practically part of the dialect too. Stripe's Markdoc and MyST push further still, adding whole component systems through named tags and directives, though both are aimed at documentation sites and their tags are wordy enough that you wouldn't want to stream them into a chat. So the appetite is proven, and so is the pattern. What's missing is the conversation-shaped version: the moment a response wants to show a metric, a small chart, a status badge, or two stats side by side, there's still little to no way to do so, and you get a wall of prose where a couple of stat cards would have landed the point at a glance.
There's already a halfway answer to this, and it's spec'd into Markdown itself: when Markdown can't express something, you can drop into raw HTML and (some) renderers pass it through. And in certain cases, that solves the richness problem. But there's a few issues with that, in order of my concerns when it comes to AI conversations & workflows:
- Most GUIs (and definitely the terminal) don't render inlined html from an agent.
- Html is token hungry.
- It's miserable to read raw html when any meaningful styling is applied.
- It's a security threat.
Plus, the power of html + css + js is overkill for a conversation and simple documentation. It doesn't need animated, interactive, on-brand interfaces for every response, and when that is needed, well... that's when I have AI write a full html page (or generate a pdf, powerpoint, etc.).
I just want something a little richer than plain Markdown, at close to plain Markdown's cost.
So the question became: why stop where GFM stopped? Do what GitHub did for tables and alerts, and keep going: give Markdown components for basic layout, information display, and input, and you get the richer response at a negligible token and legibility cost. In Supermark, that three-column layout is three characters: |||. The rule I kept coming back to, and eventually wrote at the top of the spec: if a feature can't be expressed in a handful of characters, it probably doesn't belong. A typical Supermark document lands around 2–6× smaller than the equivalent HTML for the same interface (there's a counter in the playground), and when you're streaming to users or paying per token, that compounds fast.
That sketch turned into Supermark. It isn't trying to replace real UI (it would be bad at that). It's trying to make a slightly richer Markdown response possible: keep everything that makes Markdown good in a conversation, and add just enough vocabulary to show the handful of shapes a reply actually wants.
One caveat up front: Supermark is an idea, not a proposal, and certainly not a product. A real proposal would come with the follow-through: parsers in more than one language, renderers people can actually adopt, plugins for the tools where Markdown already lives. This is earlier than that. It's something I've noticed, something I've wanted, and a sketch of what it could look like, with a playground so you can poke at it. I'm writing it up because it already makes my own reading a little better, and because I'd like to know whether it makes sense to anyone else, or whether richer AI responses should come from a different direction entirely, before deciding it deserves the follow-through.
The one-sentence version
Supermark is a superset of Markdown (GFM included) where layout, data, and interactivity are first-class, expressed in the fewest characters that still read as plain text. Every valid Markdown document is already valid Supermark. You only reach for the extra syntax when you want something Markdown can't do (a metric, a chart, a card, a callout), and when you do, it stays terse and legible.
Here's the whole idea in one example. Ask an analytics agent how the quarter's going, and this is the reply you'd want back. On the left is what the model writes. On the right is what my renderer turns it into.
# Q3 at a glance @crumbs Home / Reports / Q3 ||| @stat $128k "MRR" +18% ||| @stat 2,410 "Active teams" +6% ||| @bar Enterprise 62000 @bar Growth 41000 @bar Starter 25000 [!ok] Net revenue retention crossed 100% this quarter.
Q3 at a glance
That whole interface is a handful of lines of text a model can generate as reliably as it generates a bulleted list, for about the token cost of writing it out as prose. That's the premise: richer responses, negligible cost. From there, the interesting decisions are in the syntax itself. Given what Supermark is for, I had four priorities, and then a payoff that fell out of meeting them.
Why the syntax looks the way it does
1. It has to stream
Supermark is meant to be read as it's written, token by token, because that's how models produce it. So the grammar is line-oriented: with very few exceptions, the meaning of a line is decided by how it starts, and no line needs to know about the lines after it. A parser never has to look ahead, which means it can render a document as it arrives. The stat card appears the instant its line is complete; you're not staring at a spinner waiting for a closing tag five hundred tokens away.
2. It has to read well raw
Every construct is designed so that, stripped of a renderer, it still reads as sensible plain text. @stat $128k "MRR" +18% isn't a cryptic macro; it's a line you can understand in a raw log, a git diff, or a terminal. This is the Markdown lineage showing through: the source is never hostage to the renderer.
That also happens to smooth the only realistic path to adoption. Nothing supports Supermark on day one, so most of the time a document will land in a plain Markdown renderer (GitHub, an editor, someone's inbox), and there it degrades to legible text instead of breaking. It reads as lightly-annotated Markdown, which is a far gentler failure than what a Markdown renderer does to a page of HTML.
3. It has to be easy to remember
Most Supermark will be written by models, with a person typing the odd line by hand, and the same property serves both: ambiguous, awkward syntax is syntax that gets produced wrong. The ergonomics of the characters themselves matter.
So the sigils are deliberate. Block-level things that own a whole line start with @: @stat, @bar, @chart. Inline things you drop into a sentence use square brackets, which your eye (and a model's training data) already associates with Markdown links: [badge.green]Live[/]. Variants chain with dots, borrowed straight from CSS and Pug ([badge.green.lg.outline]), because that's a pattern models have seen a million times.
Status: [badge.green]Passing[/] [badge.amber.outline]Flaky[/] [badge.red]Down[/] Press [kbd]⌘K[/] to search. Rated [stars 4.5] by the team. Deploying [spinner] please wait…
Status: Passing Flaky Down
Press ⌘K to search. Rated ★★★★★★★★★★ by the team.
Deploying please wait…
None of that needs a manual. It reads as what it is.
4. It has to be easy to render, and the renderer decides how it looks
Supermark markup is semantic, not pixel-precise. A @stat says "this is a KPI with a label and a delta." It does not say what colour, font, or corner radius to use. That's the renderer's call.
I care about this one because it's why the examples on this page look like my site and not like a generic component kit. The exact same Supermark, handed to a different renderer, would wear that renderer's design language. The document describes intent; the host owns the aesthetics. It also keeps a renderer small: a line-oriented grammar with a fixed vocabulary is a weekend of parsing work, not a browser engine.
5. The payoff: removing HTML entirely
This is the part I'm proudest of, and it's a consequence rather than a goal I started with. Markdown supports raw HTML passthrough for a reason: the language is thin, so inline HTML is its designed escape hatch for everything it can't express. That escape hatch is also where the trouble lives, and it nags at me more now that a model is usually in the loop. Renderers need a sanitizer standing guard, and Markdown lets you hide things: an HTML comment, a display:none span, a zero-width character. A reader never sees them, but anything parsing the raw text does. That gap between what a person reads and what a machine reads is exactly where prompt injection lives, since a document that looks innocent to you can carry a second set of instructions to the next model that reads it. And models now read documents, skills, and plugins written by strangers all day.
But follow the chain back: the only justification for the escape hatch was the richness Markdown couldn't express, and for the shapes a conversation actually wants, Supermark covers that. So the escape hatch can simply go. Supermark has no raw-HTML passthrough, no comment syntax, no hidden-metadata block. Type <!-- … --> or <script> and it renders as its own literal text, in plain sight. And the language is logic-less by design: no variables, no expressions, no conditionals, no loops. Every value in a document is a literal the model already computed, so there's no machinery for untrusted output to smuggle behaviour through and nothing for a sanitizer to catch. (My renderer builds real DOM nodes and lets the framework escape text, so even a <script> you type into the playground is just... the text <script>.)
To be clear about the scope of the claim: none of this fixes the Markdown pipelines that already exist. GitHub will keep rendering HTML for compatibility forever. The claim is that a new surface (an agent UI, a chat renderer, a harness) that speaks Supermark never has to implement HTML handling at all, because the reason to implement it is gone. Source and rendered output carry the same information, so what you read is what the model actually said. That doesn't make any given document trustworthy, but it takes away the place you'd hide the untrustworthy part.
A quick tour
Beyond the basics, there's a component for most of the shapes a real interface needs. A few favourites:
Callouts, for the four things you're always trying to say. These are GitHub's alerts in spirit, minus the blockquote wrapper:
[!info] Your export is being prepared. [!ok] Payment received, you're all set. [!warn] This action can't be undone. [!err] We couldn't reach the server.
Charts from plain CSV, no charting library, just the shape of the data:
@chart:area title="Signups this week" Mon,120 Tue,180 Wed,165 Thu,240 Fri,310
Cards and layout, for composing the above into something that feels designed:
>>> @avatar "RA" teal **Review agent** found a race condition [badge.red.sm]publisher.ts[/] Two workers can claim the same event before either marks it in-flight, so the dedupe check reads stale state under load. [btn.teal]Show the diff[/] [btn.gray.outline]Dismiss[/] <<<
Review agent found a race condition publisher.ts
Two workers can claim the same event before either marks it in-flight, so the dedupe check reads stale state under load.
Show the diff Dismiss
And two flavours of control. UI controls (tabs and toggle groups) hold local view state, so they're useful the instant they render. Input controls (text and date fields, selects, sliders, switches) render as real widgets that an agent harness can wire up to collect a structured answer from the user; on their own they're presentational, and either way the document stays logic-less:
@toggle-group [Chart] Table Raw @slider "Confidence" 0 100 80 @switch "Email me the results" on
There's more (timelines, kanban boards, calendars, trees, diffs, progress bars), but the fastest way to get a feel for it is to type.
What I changed along the way
Building a real renderer is the fastest way to find the soft spots in a spec, and I hit a few worth calling out.
The original grammar had no way to escape a sigil, which meant you literally couldn't write an article about Supermark, because [badge] always tried to become one. So I added backslash escaping: \[badge] renders as the literal text. Small thing, but you're reading it work right now.
I also rounded out the Markdown superset claim so it's actually true. A first pass had the extras but was missing pieces of plain GitHub-Flavored Markdown a model emits without thinking: images, _underscore_ emphasis, bare-URL autolinks, hard line breaks, and per-column table alignment. Those all work now, so a document that's just Markdown renders exactly as you'd expect, and you only notice Supermark when you reach for something Markdown can't do.
I also loosened the container blocks (cards, collapsibles, and columns), which used to terminate on the first blank line and so couldn't hold more than one paragraph. Now each closes on a matching tag (<<< for a card, +++ for a collapsible) or, for columns, its final ||| fence, so a card or a column can hold real, multi-paragraph content. I decided that mattered more than the tidiness of auto-closing on a blank line.
That same choice, distinct open and close markers, is also what lets containers nest: a card inside a card, an accordion inside an accordion. The parser just depth-counts matched pairs, which is unambiguous and cheap in a line-oriented grammar (it's why every bracket in every language is mirrored rather than symmetric). Lists nest the ordinary Markdown way, by indentation. Flat fences that never nest, like code blocks and dividers, stay symmetric on purpose.
A couple of honest limitations remain. The input controls are display-only in this playground: by design they're meant for a host harness to read values from, and a bare renderer like this one has nothing wired up to collect them. And "streaming-first" is a property of the grammar today more than a feature of this particular renderer. The design supports it; I haven't wired incremental rendering into the playground yet. I'd rather tell you that than pretend.
Prior art
The Markdown-extension lineage at the top of this article is half the picture; the other half comes from the app side. A2UI (Google), Adaptive Cards (Microsoft), and Slack's Block Kit let an agent drive real UI by emitting JSON against a component catalog. They're solving application interfaces, and JSON is a fine call there, but it's expensive per token, unreadable raw, and useless the moment the output needs to live as text in a doc, a diff, or an inbox.
Nothing in either lineage sits where a conversation needs: Markdown-compatible, terse enough to stream cheaply, a fixed vocabulary a model can already half-guess, and readable with no renderer at all. That's the gap Supermark is sketching at. It also composes with what already works: Mermaid lives in a fenced code block, Supermark keeps fenced code blocks, so a renderer that wants rich diagrams can hand a mermaid fence to a Mermaid renderer unchanged.
Where this could go
I'd rather be straight about the state of this. Supermark is a syntax and one reference implementation, nothing more. For it to be useful to anyone but me, the interesting work is all still ahead: parser and renderer libraries in the languages people actually generate text from, plugins for the tools where Markdown already lives (Obsidian, VS Code, the GitHub pipeline), and enough agreement on the syntax that a document written for one renderer looks right in another.
None of that happens without adoption, and adoption isn't something I can will into being from a personal site. GFM proved Markdown grows by small, adoptable increments; it also had GitHub's distribution behind it, which is the part I can't replicate. But if something like this did settle into a convention, the Markdown-heavy surfaces we already stare at all day (agent UIs, docs, issues, chat) could carry a little more structure for a lot less effort, with nobody hand-writing HTML or trusting a model to. This is my sketch of that. If the idea is right and my particular syntax is wrong, I'd be glad to see a better one win.
Try it
The best way to understand Supermark is to break it. There's a live playground where you can type Supermark on one side and watch it render on the other, load a few presets, and watch the token counter tick as you go.
Open the Supermark playground →
I'd love to know what you build with it, where it falls short, or whether you'd solve this problem a different way entirely.
The full syntax
Everything Supermark understands, in one place: terse by design, so a person or a model can write compliant Supermark without hunting through the prose above. Since Supermark is a superset of Markdown, the ordinary Markdown you already know (including GitHub-Flavored extras like tables, task lists, strikethrough, and autolinks) works as-is; what follows adds the layout, data, and component syntax on top.
Document hints: first lines, one per line
^theme:dark | light- Render on a dark or light surface.
^accent:#14b8a6- Accent color for charts, bars, progress, etc.
^density:compact | default | spacious- Vertical spacing between blocks.
Text & inline (all of Markdown, plus a few)
**bold** __bold__- Bold.
*italic* _italic_- Italic. Intra-word underscores stay literal.
`code`- Inline code.
~~strike~~- Strikethrough.
==highlight==- Highlighted (marked) text.
[text](url)- Link.
https://… <https://…>- Bare or angle-bracketed URLs autolink.
- Image. src must be http(s):, data:image/, or /path.
\[ \@ \| \*- Backslash escapes any sigil to literal text.
line ending in "\" or two spaces- Hard line break within a paragraph.
Inline components: [name.mods]…[/]
[badge.green.sm.outline]New[/]- Badge. Color + size (sm/lg) + outline, any order.
[btn.teal]Open[/]- Button (presentational).
[kbd]⌘K[/]- Keyboard key.
[tip "hover text"]term[/]- Tooltip on hover.
[color.blue]x[/] [color #f00]x[/]- Colored text, named or hex.
[bg.amber]x[/]- Highlighted background.
[progress 73]- Inline progress bar, 0–100.
[spinner]- Loading spinner.
[stars 4.5]- Star rating, 0–5, halves allowed.
[swatch #7c6aff]- Color dot.
Headings, quotes, rules
# H1 … ###### H6- Headings, levels 1–6.
> quoted line- Blockquote.
--- ***- Horizontal rule.
--- Label ---- Labeled section divider.
Lists
- item * item 1. item- Unordered or ordered list.
- nested- Indent 2 spaces per level to nest.
-[x] done -[ ] todo- Task list.
Code & tables
```lang … ```- Fenced code block (lang optional).
| a | b |- Table row.
| :-- | :-: | --: |- Separator row; colons set column alignment.
Callouts: [!kind] text
[!info] [!ok] [!warn] [!err]- Four callout styles.
Layout containers
||| … ||| … |||- Columns. Flat (no nesting); final ||| closes.
>>> … <<<- Card. Nests by depth (>>> opens, <<< closes).
+++ Title … +++- Collapsible. Title required; nests by depth.
::: center | right ::: … ::: :::- Align a block.
Stats & charts
@stat $48k "Revenue" +23%- KPI. Delta optional; a leading - reads as down/red.
@bar Label 24500- Horizontal bar; consecutive @bar lines group.
@chart:TYPE title="…"- Chart: line, area, bar, pie, scatter. Rows follow as CSV.
@metrics then Label :: value- Label/value list.
Sequence & structure: header line, then rows
@steps then 1. text- Numbered walkthrough.
@timeline then ~ Date :: text- Timeline entries.
@tree then indented names- Any nested hierarchy; 2-space indent per level.
@diff then +add -del ctx- Diff block.
@kanban then ## Column - card- Board columns and cards.
UI bits
@crumbs A / B / C- Breadcrumbs.
@avatar "IN" teal "AK" purple- Avatar group (initials + color).
@quote "Author" "Source" then body- Pull quote; both attributes optional.
@empty "No results" 🔍- Empty state (icon optional).
@skeleton lines:3 | card | avatar- Loading placeholder.
@pages 1 2 [3] 4- Pagination; [n] marks the active page.
@calendar 2026-02 highlight=[3,7]- Month grid with highlighted days.
UI controls: local view state, no data binding
@tabs A | B then @tab A / content- Tabbed panels.
@toggle-group [A] B C- Segmented control; [x] marks the active option.
Input controls
Presentational on their own. They’re meant to be wired by an agent harness that reads the values the user enters; the document itself stays logic-less and never submits.
@input "Label" email "placeholder"- Text field (any HTML input type).
@textarea "Label" 4 "placeholder"- Multi-line field (rows count).
@select "Label" [A (B) C]- Dropdown; (x) marks the default.
@radio "Label" [A (B)]- Radio group; (x) marks the default.
@switch "Label" on | off- Toggle.
@slider "Label" 0 100 80- Slider: min, max, value.
@date "Label" 2026-03-01- Date field.
Rules that aren’t obvious from the syntax
- Line-oriented: a block’s meaning is set by how its line starts. Block sigils (#, @, |||, >>>, +++, :::) own the whole line; inline components live inside text.
- Every valid Markdown (and GitHub-Flavored Markdown) document is already valid Supermark.
- Containers close on a matching token, so they can hold multiple paragraphs. Cards and collapsibles nest by depth-counting; columns stay flat.
- A collapsible’s opening +++ must carry a title; the bare +++ closes it.
- Logic-less by design: every value is a literal, with no variables, expressions, or loops. UI controls (tabs, toggle-group) hold local view state; input controls are read by a host harness rather than submitting anywhere.
- No hidden content: there’s no comment, metadata, or raw-HTML syntax, so a document can’t carry anything that renders invisibly; the source and the rendered output say the same thing.