Skip to content

MDX components

What each Markdown element of a page becomes, Prose for the typography of the content, code block tabs, and Callout, Cards, Kbd, Preview and Tabs on every page.

@contour-ui/fumadocs/mdx draws the MDX of a page with the styled components. getMdxComponents() returns the components that the Markdown elements render with, and Prose gives the text around them its typography:

import { getMdxComponents, Prose } from '@contour-ui/fumadocs/mdx';

<Prose>
  <Mdx components={getMdxComponents()} />
</Prose>;

The components

In the MDXRenders as
headings, # to ######the heading, whose text links to its own #id, with a hash that shows on hover and focus
a link to a page of the sitethe styled Link, underlined, through the Link of the framework
a link to a #headingthe styled Link, as a plain link
a link to another sitethe styled Link, which opens in a new tab, with an arrow named "opens in a new tab"
tablesa Table, outlined, scrolling sideways when wider than the column
fenced codea CodeBlock: the title, the highlighted code and a copy button
fenced code with a tabthe tabs of one code block, see Code block tabs
```npm, package-installthe command of each package manager, see Package managers
<Tabs>, <Tab>the styled Tabs in a frame, see Tabs
<Callout>an Alert, see Callout
<Cards>, <Card>a grid of styled cards, see Cards
<Kbd>a Kbd: ⌘ K
<Preview>the frame of a live example, see Preview

The components in angle brackets need no import in a page. Other elements, such as lists, quotes and images, render as they are, and Prose styles them. A lowercase tag in MDX, such as <kbd>, is a plain HTML element and skips the map: write <Kbd>.

Code blocks

Fumadocs highlights fenced code as it compiles the page, with Shiki: give it the css-variables theme, as Setup shows, and the code takes the syntax colors of the theme it is shown in. The meta of a block, after its language, sets the rest:

MetaEffect
title="app.ts"a header with the title, and the copy button in it
noCopyno copy button
lineNumbersa number before each line
lineNumbers=10numbers from 10
page.mdx
```tsx title="app.tsx" lineNumbers
export function App() {
  return <Button>Save</Button>;
}
```

The notation comments of Shiki, such as // [!code highlight] and // [!code ++], mark lines as the CodeBlock page shows.

Code block tabs

Consecutive code blocks with a tab meta become the tabs of one block. remarkCodeTab of Fumadocs, which fumadocs-mdx runs by default, writes them into the page as CodeBlockTabs, and the map draws them: one frame, the triggers where a code block has its header, the code of the selected tab under them, and its copy button at the end of the row of triggers.

page.mdx
```tsx tab="React Router"
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
```

```tsx tab="Next.js" title="app/layout.tsx"
import { NextProvider } from 'fumadocs-core/framework/next';
```
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
  • The meta of each block stays its own: a block with a title keeps its header under the row of triggers, and noCopy leaves its copy button out.
  • tab-group="framework" on the first block of the tabs makes them one group with the other blocks and Tabs of that id, remembered across visits: the reader picks a framework once, for the whole site.
  • In <Tabs>, code blocks with a tab meta become its Tabs instead, by the item they name.

Package managers

A code block in the npm language, or package-install with only the packages, becomes the commands of npm, pnpm, yarn and bun, as code block tabs. remarkNpm of Fumadocs writes them, and fumadocs-mdx runs it by default too:

page.mdx
```package-install
@contour-ui/react @contour-ui/tokens
```
npm install @contour-ui/react @contour-ui/tokens

Its persist option makes the tabs of every such block one group, remembered across visits, as on this site, where the package manager you pick shows in every install command:

vite.config.ts
fumadocsMdx({
  globalOptions: {
    mdxOptions: {
      remarkNpmOptions: { persist: { id: 'package-manager' } },
    },
  },
});

The components are those of Fumadocs, with its props, so the output of both plugins renders as it is: CodeBlockTabs (defaultValue, and groupId, persist and updateAnchor as in Tabs), CodeBlockTabsList, CodeBlockTabsTrigger (value) and CodeBlockTab (value).

Tabs

Language
Import the components, and pass them their props.
page.mdx
<Tabs items={['JavaScript', 'TypeScript']} label="Language" updateAnchor>
  <Tab id="tabs-javascript">Import the components, and pass them their props.</Tab>
  <Tab id="tabs-typescript">Import the components and the types of their props…</Tab>
</Tabs>

Tabs shows sections of content one at a time: the styled Tabs in a frame, the row of triggers at the top and the selected panel under it, with the props of the Tabs of Fumadocs. items makes a trigger for each name, and each Tab holds the content of one: its value is the item, as written, or, without one, the item of its place among the children. A code block alone in a Tab fills it, and its copy button moves up into the row of triggers.

PropTypeDefault
itemsstring[]: the names of the tabs, a trigger each—
defaultIndexnumber: the index of the item selected at first0
defaultValuestring: the value selected at first, in place of defaultIndex—
labelReactNode: a label at the start of the row, which names the tabs—
groupIdstring: the group of the tabs—
persistboolean: the group remembers the tab across visitsfalse
updateAnchorboolean: a tab the reader selects puts its id in the addressfalse

Tab takes value and the props of its div, such as id. Other props of Tabs go to its root, a div. The keys are those of the styled Tabs: the arrow keys move along the triggers and select, Home and End go to the ends.

Groups and storage

  • A group. Tabs with the same groupId, on the page and on the next pages, select the same tab: a reader's choice in one selects it in the others that have it, and the others keep theirs. Tabs, code block tabs and their tab-group share the groups.
  • For the session. A group remembers the tab in sessionStorage, under its id, so the next pages open on it.
  • Across visits with persist: in localStorage too, under its id. The keys are those of fumadocs-ui, so the readers of a site that moves over keep their choice.
  • Before hydration the server renders the default tab: the remembered one follows as the page hydrates.
  • The address opens a tab. #tabs-typescript, the id of a Tab, selects it and scrolls the tabs into view; an address that points into a tab, such as a heading inside it, selects that tab and scrolls to the element. Try it: TypeScript.
  • updateAnchor puts the id of the tab the reader selects in the address, replacing the entry of the history, so the address links to it. A tab without an id leaves the address alone.

Parts

Without items, build the tabs part by part, as remarkCodeTab does with its parseMdx option: TabsList holds the TabsTriggers (value), and each TabsContent (value) the content of one. defaultValue selects one at first.

<Tabs defaultValue="props">
  <TabsList aria-label="Reference">
    <TabsTrigger value="props">Props</TabsTrigger>
    <TabsTrigger value="events">Events</TabsTrigger>
  </TabsList>
  <TabsContent value="props">The props of the component.</TabsContent>
  <TabsContent value="events">What it reports.</TabsContent>
</Tabs>

Your own components

getMdxComponents(overrides) adds your components to the map, so every page can use them without an import, or replaces the ones above. The overrides come last:

import { getMdxComponents } from '@contour-ui/fumadocs/mdx';

import { Figure } from './figure';
import { Steps } from './steps';

<Mdx components={getMdxComponents({ Steps, img: Figure })} />;

getMdxComponents(), Prose and Preview are server modules, and the components of the map that need the browser are client modules ('use client'): the MDX of a page renders as a React Server Component too, as on Next.js. There, overrides are server components, the components of @contour-ui/react, or named exports of client modules: a map built on the server cannot read inside an export of a client module of your own (contour-ui in server components).

Prose

Prose gives the MDX content its typography and its rhythm: the fonts of the headings, the text and the code, the space between blocks, the markers of lists. It relies on no reset: it sets the margins, paddings and fonts of the elements it styles over the defaults of the browser.

  • Components keep their styles. Every rule of Prose has zero specificity (:where()) and sits in a cascade layer below that of the components, so a styled component in the content keeps its own look, and any rule of yours overrides Prose.
  • Fonts are set on the root, the headings, inline code and captions only: text inside a component keeps the component's font.
  • Opting out. Prose leaves alone whatever is inside an element with the contour-docs-not-prose class, and still spaces that element as a block. Preview uses it.
<Prose>
  <p>Typography from Prose.</p>
  <div className="contour-docs-not-prose">
    <p>Only its own styles.</p>
  </div>
</Prose>

Prose renders a div and takes its props; your className joins contour-docs-prose. It imports nothing from @contour-ui/react, so it renders on the server and adds nothing to the code of the browser.

Preview

Preview frames live examples: a bordered panel on the canvas color, its children side by side, wrapping when the row is full. Its content is outside Prose (contour-docs-not-prose), so components render with their own styles only. A page imports the components it shows at its top:

button.mdx
import { Button } from '@contour-ui/react';

<Preview>
  <Button>Save</Button>
  <Button variant="outline">Cancel</Button>
</Preview>

Preview renders a div and takes its props, and renders on the server too. On Next.js, the examples inside come from client modules (Setup).

Callout

<Callout type="warn" title="Before the component styles">
  Load the stylesheet of this package first.
</Callout>

Callout is an Alert: the icon of its status, the title, then the content, which may hold paragraphs. Its props are those of the callout of Fumadocs, so pages written for fumadocs-ui render as they are:

PropTypeDefault
type'info', 'warn', 'warning', 'error', 'success', 'idea''info'
titleReactNode—
iconReactNode: replaces the icon of the status—
typeStatus of the alert
infoinfo
warn, warningwarning
errordanger
successsuccess
ideaneutral

Other props go to the root of the alert, a div.

Cards

<Cards>
  <Card title="Setup" href="/docs/fumadocs/setup">
    Install the package and wire it into your framework.
  </Card>
  <Card title="Fumadocs" href="https://fumadocs.dev" description="The framework of the content." />
</Cards>

Cards lays its cards out in a grid: one column on phones, two from 40rem. Card is the styled Card, with the props of the card of Fumadocs:

PropTypeDefault
titleReactNoderequired
descriptionReactNode: the text under the title; or the children—
hrefstring: makes the whole card one link—
externalboolean: opens href in a new tab, a path tooa URL with a scheme opens in a new tab
iconReactNode: shown above the title—

A path goes through the Link of the framework. The title looks like the title of the styled Card but is no heading: cards follow a heading of the page, and a level of their own would break the outline of the page.