Generative UI for LLMs using Markdoc {% %} tag syntax
概览
-- mdocUI is under active development. The API may change between minor versions. We follow semver and will reach 1.0 once the API stabilizes. Generative UI library for LLMs using Markdoc {% %} tag syntax inline with markdown prose. LLMs write natural markdown drop interactive UI components in the same stream — charts, buttons, forms, tables, cards, and more. No custom DSL to learn, no JSON blocks, no JSX confusion. built-in prose rendering, component merging, CLI scaffolder, error boundaries, streaming animations, shimmer placeholders, prop validation, context data passthrough, configurable prompt verbosity, and accessible components. mdocUI combines two syntaxes in a single stream: - -- the universal prose format created by John Gruber (2004), standardized by CommonMark. Handles headings, bold, italic, lists, links, code blocks -- everything an LLM already knows how to write. - -- the tag syntax from Stripe's Markdoc framework (2022, MIT).
README
Documentation · Live Demo · npm
Alpha – mdocUI is under active development. The API may change between minor versions. We follow semver and will reach 1.0 once the API stabilizes.
Generative UI library for LLMs using Markdoc {% %} tag syntax inline with markdown prose.
LLMs write natural markdown and drop interactive UI components in the same stream — charts, buttons, forms, tables, cards, and more. No custom DSL to learn, no JSON blocks, no JSX confusion.
Key features: built-in prose rendering, component merging, CLI scaffolder, error boundaries, streaming animations, shimmer placeholders, prop validation, context data passthrough, configurable prompt verbosity, and accessible components.
The Q4 results show strong growth across all segments.
{% chart type="bar" labels=["Jan","Feb","Mar"] values=[120,150,180] /%}
Revenue grew **12%** quarter-over-quarter.
{% callout type="info" title="Action Required" %}
Review the pipeline before end of quarter.
{% /callout %}
{% button action="continue" label="Show by region" /%}
{% button action="continue" label="Export as PDF" /%}
Syntax: Markdown + Markdoc Tags
mdocUI combines two syntaxes in a single stream:
- Markdown – the universal prose format created by John Gruber (2004), standardized by CommonMark. Handles headings, bold, italic, lists, links, code blocks – everything an LLM already knows how to write.
- Markdoc
{% %}tags – the tag syntax from Stripe’s Markdoc framework (2022, MIT). Markdoc extends Markdown with{% %}custom tags for structured content.
mdocUI borrows only the {% %} tag syntax from Markdoc. We do not use Markdoc’s parser, runtime, compiler, schema system, or config layer. We built our own streaming parser from scratch, purpose-built for token-by-token LLM output.
Tag forms
Self-closing (no body):
{% tagname attr="value" /%}
With body content:
{% tagname attr="value" %}
Body content here -- can include markdown or nested tags.
{% /tagname %}
Why {% %} works for streaming
The character sequence {% never appears in normal prose, standard markdown, or fenced code blocks. This makes it a reliable delimiter that a character-by-character streaming parser can detect without ambiguity – no lookahead, no backtracking, no fragile heuristics.
The LLM writes both markdown and component tags in the same response. The parser separates them into prose nodes and component nodes as tokens arrive.
Why mdocUI?
| Approach | Prose? | Components? | Streaming? | Token efficient? |
|---|---|---|---|---|
| Plain markdown | Yes | No | Yes | Yes |
| OpenUI Lang | No | Yes | Yes | Yes |
| JSON blocks in markdown | Yes | Yes | Fragile | No |
| JSX in markdown | Yes | Yes | Fragile | No |
| mdocUI | Yes | Yes | Yes | Yes |
Markdoc’s {% %} delimiters are unambiguous — they never appear in normal prose or code, making streaming parsing reliable.
Packages
| Package | Description | Status |
|---|---|---|
@mdocui/core |
Streaming parser, tokenizer, component registry, prompt generator | Alpha |
@mdocui/react |
React renderer, 24 default components, useRenderer hook |
Alpha |
@mdocui/web-components |
`` custom element — any framework, or none | Alpha |
@mdocui/cli |
Scaffold, generate system prompts, preview | Alpha |
Quick Start
pnpm add @mdocui/core @mdocui/react
1. Generate a system prompt
generatePrompt() merges two layers into one prompt: the library layer (tag syntax, component signatures, composition rules — auto-generated from the registry) and your app layer (preamble, domain rules, examples). You never write syntax docs manually.
import { generatePrompt } from '@mdocui/core'
import { createDefaultRegistry, defaultGroups } from '@mdocui/react'
const registry = createDefaultRegistry()
const systemPrompt = generatePrompt(registry, {
preamble: 'You are a helpful assistant.',
groups: defaultGroups,
})
// Pass systemPrompt to your LLM
2. Render streamed output
import { useRenderer } from '@mdocui/react'
import { Renderer, defaultComponents, createDefaultRegistry } from '@mdocui/react'
const registry = createDefaultRegistry()
function Chat() {
const { nodes, isStreaming, push, done } = useRenderer({ registry })
// Call push(chunk) as tokens arrive from LLM
// Call done() when stream ends
// useRenderer batches renders to at most one per frame (~60fps) automatically
return (
<Renderer
nodes={nodes}
components={defaultComponents}
isStreaming={isStreaming}
onAction={(event) => {
if (event.action === 'continue') {
sendMessage(event.label)
}
}}
onError={(event) => {
console.error(`Component ${event.componentName} failed:`, event.error)
}}
/>
)
}
3. Handle actions and errors
Every interactive component fires through a single onAction callback:
onAction={(event) => {
switch (event.action) {
case 'continue':
// Send event.label as a new user message
break
case 'submit:formName':
// event.formState has all field values
break
case 'open_url':
// event.params.url has the URL
break
}
}}
Catch component rendering errors with onError:
onError={(event) => {
console.error(`${event.componentName} failed to render:`, event.error)
// event.props contains the props that caused the error
}}
Custom Components
Every component receives ComponentProps and can be swapped:
interface ComponentProps {
name: string
props: Record
children?: React.ReactNode
className?: string
onAction: ActionHandler
isStreaming: boolean
}
Override specific components
import { defaultComponents, Renderer } from '@mdocui/react'
const myComponents = {
...defaultComponents,
button: MyButton, // swap just the button
card: MyShadcnCard, // use your shadcn card
}
Tailwind / className support
Pass per-component classes via classNames:
<Renderer
nodes={nodes}
components={defaultComponents}
classNames={{
button: 'bg-blue-500 hover:bg-blue-600 text-white rounded-lg px-4 py-2',
card: 'border border-gray-200 rounded-xl p-6 shadow-sm',
callout: 'border-l-4 pl-4 py-3',
}}
/>
Bring your own components entirely
const shadcnComponents = {
button: ({ props, onAction }) => (
onAction({ type: 'button_click', action: props.action, label: props.label, tagName: 'button' })}>
{props.label}
),
card: ({ props, children }) => (
{props.title}{children}
),
}
Architecture
| Layer | Role |
|---|---|
| Tokenizer | Character-by-character lexer, tracks IN_PROSE / IN_TAG / IN_STRING states |
| StreamingParser | Buffers incomplete tags, merges prose, emits ASTNode[] |
| ComponentRegistry | Validates tag names and props via Zod schemas |
| Renderer | Maps AST nodes to React components with error boundaries and animations |
The core is framework-agnostic. @mdocui/react is one adapter — Vue, Svelte, and Angular adapters can follow the same pattern.
Available Components
Layout
stack grid card divider accordion tabs tab
Interactive
button button-group input textarea select checkbox toggle form
Data
chart table stat progress
Content
callout badge image code-block link
All components render theme-neutral semantic HTML with data-mdocui-* attributes. They use currentColor and inherit — no hardcoded colors. They adapt to any light or dark theme automatically. Style them with CSS, Tailwind classNames, or swap in your own components entirely.
Development
# Install
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test
# Lint
pnpm lint
# Run playground
pnpm playground
Contributing
Contributions are welcome! See CONTRIBUTING.md for the full guide.
- Fork the repo
- Create a feature branch (
git checkout -b feature/my-change) - Make your changes with tests
- Run
pnpm test && pnpm lint - Open a pull request
For AI Agents
- SKILL.md — implementation guide for AI coding agents
- llms.txt — machine-readable project summary for LLM crawlers
Install as a Claude Code skill:
Project-level (current project only):
mkdir -p .claude/skills/mdocui
curl -o .claude/skills/mdocui/SKILL.md https://raw.githubusercontent.com/mdocui/mdocui/main/SKILL.md
Personal (available in all your projects):
mkdir -p ~/.claude/skills/mdocui
curl -o ~/.claude/skills/mdocui/SKILL.md https://raw.githubusercontent.com/mdocui/mdocui/main/SKILL.md
Then invoke with /mdocui in Claude Code.
Roadmap
Renderers
@mdocui/vue— Vue 3 renderer@mdocui/svelte— Svelte 5 renderer@mdocui/solid— SolidJS renderer
Integrations
@mdocui/nextjs— App Router helpers, RSC-safe imports, route handler template@mdocui/vercel-ai— Vercel AI SDKuseChattouseRendererbridge
Developer tools
@mdocui/devtools— browser panel for AST inspection, parse meta, streaming state- VS Code extension — syntax highlighting and autocomplete for
{% %}tags
Milestone
- v1.0.0 — stable API, frozen component props, CHANGELOG
Have an idea? Open a suggestion issue.
License
推荐工具
换一个关键词,或者移除筛选条件。
安装
npx skillfish add mdocui/mdocui