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.
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.
# 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
| Write | Result |
|---|---|
**bold** | bold |
*italic* | italic |
~~strikethrough~~ | |
`inline code` | inline code |
line (two trailing spaces) | a hard line break |
Links and images
[OTF Web on GitHub](https://github.com/opentf/Web-App-Framework) 
Renders:

Lists
Unordered (- or *), ordered (1.), and nested. An ordered list can start at a custom number.
- First - Second - Nested 1. One 2. Two
Renders:
First
Second
Nested
One
Two
Task lists
GFM checkboxes render as disabled inputs:
- [x] Scaffold the project - [x] Write some docs - [ ] Ship it
Renders:
-
Scaffold the project
-
Write some docs
-
Ship it
Blockquotes
> A short, quoted aside.
A short, quoted aside.
Horizontal rule
Three dashes on their own line draw a divider (<hr/>):
above --- below
Renders:
above
below
Tables
A pipe table with a header row:
| Package | Purpose | | --- | --- | | web | Runtime + signals | | web-docs | Docs site generator |
| Package | Purpose |
|---|---|
| web | Runtime + signals |
| web-docs | Docs site generator |
Use a colon in the delimiter row to set per-column alignment — :-- left, :-: center, --: right:
| Left | Center | Right | | :--- | :----: | ----: | | web | runtime | 0.5.0 | | web-docs | generator | 0.1.0 |
| Left | Center | Right |
|---|---|---|
| web | runtime | 0.5.0 |
| web-docs | generator | 0.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.
```js app.js const count = $state(0); ```
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.
--- title: Installation description: Scaffold a project and start the dev server. ---
Imports
Pull in components (or anything else) with a normal import:
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:
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/webnpm i @opentf/webComponents inline
Any imported component can be dropped into the prose like an HTML tag, and Markdown inside its children still renders:
<Callout type="tip" title="Tip"> You can use **Markdown** inside a component's children. </Callout>
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.
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.
The answer is {2 + 2}.
Renders:
The answer is 4.
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.