| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
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, and configurable prompt verbosity.
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" /%}
mdocUI combines two syntaxes in a single stream:
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.
Self-closing (no body):
{% tagname attr="value" /%}
With body content:
{% tagname attr="value" %}
Body content here -- can include markdown or nested tags.
{% /tagname %}
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.
| 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.
| Package | Description | Status |
|---|---|---|
| @mdocui/core | Streaming parser, tokenizer, component registry, prompt generator | Alpha |
| @mdocui/react | React renderer, 24 default components, useRenderer hook | Alpha |
| @mdocui/cli | Scaffold, generate system prompts, preview | Alpha |
pnpm add @mdocui/core @mdocui/reactgeneratePrompt() 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 LLMimport { 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)
}}
/>
)
}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
}}Every component receives ComponentProps and can be swapped:
interface ComponentProps {
name: string
props: Record<string, unknown>
children?: React.ReactNode
className?: string
onAction: ActionHandler
isStreaming: boolean
}import { defaultComponents, Renderer } from '@mdocui/react'
const myComponents = {
...defaultComponents,
button: MyButton, // swap just the button
card: MyShadcnCard, // use your shadcn card
}
<Renderer nodes={nodes} components={myComponents} />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',
}}
/>const shadcnComponents = {
button: ({ props, onAction }) => (
<Button onClick={() => onAction({ type: 'button_click', action: props.action, label: props.label, tagName: 'button' })}>
{props.label}
</Button>
),
card: ({ props, children }) => (
<Card><CardHeader>{props.title}</CardHeader><CardContent>{children}</CardContent></Card>
),
}
<Renderer nodes={nodes} components={shadcnComponents} />| 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.
stack grid card divider accordion tabs tab
button button-group input textarea select checkbox toggle form
chart table stat progress
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.
# Install
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test
# Lint
pnpm lint
# Run playground
pnpm playgroundContributions are welcome! See CONTRIBUTING.md for the full guide.
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.mdPersonal (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.mdThen invoke with /mdocui in Claude Code.
Renderers
Integrations
Developer tools
Milestone
Have an idea? Open a suggestion issue.
| Back | FazBrowse Home | New Git URL |