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 MDX | Renders 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 site | the styled Link, underlined, through the Link of the framework |
a link to a #heading | the styled Link, as a plain link |
| a link to another site | the styled Link, which opens in a new tab, with an arrow named "opens in a new tab" |
| tables | a Table, outlined, scrolling sideways when wider than the column |
| fenced code | a CodeBlock: the title, the highlighted code and a copy button |
fenced code with a tab | the tabs of one code block, see Code block tabs |
```npm, package-install | the 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:
| Meta | Effect |
|---|---|
title="app.ts" | a header with the title, and the copy button in it |
noCopy | no copy button |
lineNumbers | a number before each line |
lineNumbers=10 | numbers from 10 |
```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.
```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';import { NextProvider } from 'fumadocs-core/framework/next';- The meta of each block stays its own: a block with a
titlekeeps its header under the row of triggers, andnoCopyleaves its copy button out. tab-group="framework"on the first block of the tabs makes them one group with the other blocks andTabsof that id, remembered across visits: the reader picks a framework once, for the whole site.- In
<Tabs>, code blocks with atabmeta become itsTabs 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:
```package-install
@contour-ui/react @contour-ui/tokens
```npm install @contour-ui/react @contour-ui/tokenspnpm add @contour-ui/react @contour-ui/tokensyarn add @contour-ui/react @contour-ui/tokensbun add @contour-ui/react @contour-ui/tokensIts 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:
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
Import the components and the types of their props: every component exports the type of its props next to it.
<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.
| Prop | Type | Default |
|---|---|---|
items | string[]: the names of the tabs, a trigger each | — |
defaultIndex | number: the index of the item selected at first | 0 |
defaultValue | string: the value selected at first, in place of defaultIndex | — |
label | ReactNode: a label at the start of the row, which names the tabs | — |
groupId | string: the group of the tabs | — |
persist | boolean: the group remembers the tab across visits | false |
updateAnchor | boolean: a tab the reader selects puts its id in the address | false |
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 theirtab-groupshare 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: inlocalStoragetoo, under its id. The keys are those offumadocs-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.
Links to a tab
- The address opens a tab.
#tabs-typescript, theidof aTab, 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. updateAnchorputs theidof the tab the reader selects in the address, replacing the entry of the history, so the address links to it. A tab without anidleaves 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-proseclass, and still spaces that element as a block.Previewuses 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:
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:
| Prop | Type | Default |
|---|---|---|
type | 'info', 'warn', 'warning', 'error', 'success', 'idea' | 'info' |
title | ReactNode | — |
icon | ReactNode: replaces the icon of the status | — |
type | Status of the alert |
|---|---|
info | info |
warn, warning | warning |
error | danger |
success | success |
idea | neutral |
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:
| Prop | Type | Default |
|---|---|---|
title | ReactNode | required |
description | ReactNode: the text under the title; or the children | — |
href | string: makes the whole card one link | — |
external | boolean: opens href in a new tab, a path too | a URL with a scheme opens in a new tab |
icon | ReactNode: 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.