An open API service indexing awesome lists of open source software.

https://github.com/mdocui/mdocui

Generative UI for LLMs using Markdoc {% %} tag syntax
https://github.com/mdocui/mdocui

ai generative-ui llm markdoc react streaming ui-components

Last synced: 3 months ago
JSON representation

Generative UI for LLMs using Markdoc {% %} tag syntax

Awesome Lists containing this project

README

          


mdocUI


npm core
npm react
npm cli
CI
CodeQL
MIT License
TypeScript
Node
Alpha
Sponsor pnutmath


Documentation · Live Demo · npm

> **Alpha** -- mdocUI is under active development. The API may change between minor versions. We follow [semver](https://semver.org/) 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.


mdocUI demo — streaming dashboard with charts, stats, and tables

```
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](https://commonmark.org/)** -- the universal prose format created by John Gruber (2004), standardized by [CommonMark](https://github.com/commonmark/commonmark-spec). Handles headings, bold, italic, lists, links, code blocks -- everything an LLM already knows how to write.
- **[Markdoc](https://github.com/markdoc/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`](packages/core) | Streaming parser, tokenizer, component registry, prompt generator | Alpha |
| [`@mdocui/react`](packages/react) | React renderer, 24 default components, `useRenderer` hook | Alpha |
| [`@mdocui/cli`](packages/cli) | Scaffold, generate system prompts, preview | Alpha |

## Quick Start

```bash
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.

```typescript
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

```tsx
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 (
{
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:

```typescript
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`:

```typescript
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:

```typescript
interface ComponentProps {
name: string
props: Record
children?: React.ReactNode
className?: string
onAction: ActionHandler
isStreaming: boolean
}
```

### Override specific components

```tsx
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`:

```tsx

```

### Bring your own components entirely

```tsx
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

```bash
# 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](CONTRIBUTING.md) for the full guide.

1. Fork the repo
2. Create a feature branch (`git checkout -b feature/my-change`)
3. Make your changes with tests
4. Run `pnpm test && pnpm lint`
5. Open a pull request

## For AI Agents

- [SKILL.md](SKILL.md) — implementation guide for AI coding agents
- [llms.txt](https://mdocui.github.io/llms.txt) — machine-readable project summary for LLM crawlers

**Install as a Claude Code skill:**

Project-level (current project only):
```bash
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):
```bash
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 SDK `useChat` to `useRenderer` bridge

**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](https://github.com/mdocui/mdocui/issues/new?labels=enhancement&title=Suggestion:+).

## License

[MIT](LICENSE)