Markdown & MDX

Docs pages are .mdx files: Markdown for prose plus JSX components inline. The compiler turns them into native DOM at build time — there is no Markdown parser in the browser. This page lists everything you can write.

Three things happen automatically

Every heading gets a slug id and joins the On this page outline; fenced code is syntax-highlighted at build time; and each code block gets a copy button. You don't wire any of it.

Markdown

OTF Web supports CommonMark plus the common GitHub-Flavored Markdown (GFM) extensions.

Headings

# through ###### become <h1><h6>. Each is slugified into an id so it's linkable; the ## and ### levels also populate the On this page outline.

Markdown
# Heading level 1
## Heading level 2
### Heading level 3
#### Heading level 4
##### Heading level 5
###### Heading level 6

Renders:

Heading level 1

Heading level 2

Heading level 3

Heading level 4

Heading level 5
Heading level 6

Text formatting

WriteResult
**bold**bold
*italic*italic
~~strikethrough~~strikethrough
`inline code`inline code
line (two trailing spaces)a hard line break
Markdown
[OTF Web on GitHub](https://github.com/opentf/Web-App-Framework)
![OTF Web logo](/logo.png "OTF Web")

Renders:

OTF Web on GitHub

OTF Web logo

Lists

Unordered (- or *), ordered (1.), and nested. An ordered list can start at a custom number.

Markdown
- First
- Second
  - Nested
1. One
2. Two

Renders:

  • First

  • Second

    • Nested

  1. One

  2. Two

Task lists

GFM checkboxes render as disabled inputs:

Markdown
- [x] Scaffold the project
- [x] Write some docs
- [ ] Ship it

Renders:

  • Scaffold the project

  • Write some docs

  • Ship it

Blockquotes

Markdown
> A short, quoted aside.

A short, quoted aside.

Horizontal rule

Three dashes on their own line draw a divider (<hr/>):

Markdown
above

---

below

Renders:

above


below

Tables

A pipe table with a header row:

Markdown
| Package | Purpose |
| --- | --- |
| web | Runtime + signals |
| web-docs | Docs site generator |
PackagePurpose
webRuntime + signals
web-docsDocs site generator

Use a colon in the delimiter row to set per-column alignment — :-- left, :-: center, --: right:

Markdown
| Left | Center | Right |
| :--- | :----: | ----: |
| web | runtime | 0.5.0 |
| web-docs | generator | 0.1.0 |
LeftCenterRight
webruntime0.5.0
web-docsgenerator0.1.0

Every table ships inside a horizontally scrollable container, so a table too wide for the page scrolls itself instead of overflowing the content column.

Code blocks

A fenced block is highlighted at build time (syntect) and decorated with a header and copy button. Put a language after the fence, and optionally a filename — the rest of the info string becomes the title shown in the header.

Markdown
```js app.js
const count = $state(0);
```
JavaScriptapp.js
const count = $state(0);

Language aliases are handled, so jsx, tsx, ts, and friends all highlight (they map to the closest available grammar).

MDX

MDX is Markdown plus JavaScript modules and JSX. These features turn a static page into a live one.

Frontmatter

A YAML block at the very top sets the page's metadata — title and description feed the sidebar, the document title, and search.

MDX
---
title: Installation
description: Scaffold a project and start the dev server.
---

Imports

Pull in components (or anything else) with a normal import:

MDX
import { Callout, Tabs } from "@opentf/web-docs";

Exported data

Declare data with export const and reference it below. This is also how you pass rich values to a component, since JSX nodes are valid here:

MDX
export const tabs = [
  { label: "bun", content: <CodeBlock code="bun add @opentf/web" /> },
  { label: "npm", content: <CodeBlock code="npm i @opentf/web" /> },
];

<Tabs tabs={tabs} />

Rendered:

bun add @opentf/web
npm i @opentf/web

Components inline

Any imported component can be dropped into the prose like an HTML tag, and Markdown inside its children still renders:

MDX
<Callout type="tip" title="Tip">
  You can use **Markdown** inside a component's children.
</Callout>
Tip

You can use Markdown inside a component's children.

Interactive components

Components compile to real custom elements backed by signals, so you can define one right in the page with export function and use it inline — $state makes it interactive, no extra wiring. Markdown still works in the children.

MDX
export function Counter(props) {
  let count = $state(0);
  return (
    <button onclick={() => (count += 1)}>
      {props.children} {count}
    </button>
  );
}

<Counter>**Clicks**: </Counter>

Renders (click it):

Expressions

A { } expression is evaluated as JavaScript and rendered inline.

MDX
The answer is {2 + 2}.

Renders:

The answer is 4.

Inline object & array literals in attributes

A component attribute can't take an inline object/array literal (tabs={[{ label: ... }]}) — the MDX parser rejects the commas. Declare the value with export const and pass it by name instead, as shown above.