Components
The complete component vocabulary, rendered on the page that documents it, including callouts, cards, steps, tabs, and trees.
Prefer a semantic PascalCase tag over styling content with className. The
complete vocabulary is declared in
src/components/mdx-components.ts,
with one line per author-facing name. An unknown tag fails the build and names
itself in the error.
Everything on this page is live. Each example shows exactly what the component does.
The vocabulary
| Group | Tags |
|---|---|
| Structure | Tabs, Tab, CodeGroup, CodeBlock, Steps, Step, Columns, Column, Panel, Frame |
| Emphasis | Callout, Note, Warning, Info, Tip, Check, Danger, Banner, Badge, Update, Tooltip |
| Content and API | Prompt, ParamField, ResponseField, RequestExample, ResponseExample, Examples, Endpoint |
| Disclosure | Accordion, AccordionGroup, Expandable |
| Audience and variants | View, Visibility |
| Navigation and visuals | Card, CardGroup, Tile, Tree, TreeFolder, TreeFile, Icon, Mermaid, Color, ColorRow, ColorItem |
| Actions | CTA, Button |
Some tags have shorter aliases. Duvlify keeps them because they match the
notation most documentation already uses: FileTree/Folder/File for the
tree, Response for ResponseField, LinkButton for Button, and
Color.Row/Color.Item and Tree.Folder/Tree.File as member expressions of
their parent. Each alias is the same component as its full name.
Callouts
A callout is one bordered, tinted aside. The tone prop selects both the tint
and the icon. Note, Tip, Info, Check, Warning, and Danger are
one-line wrappers that each pick a tone, so a page never has to name a colour
directly.
<Callout title="A callout with a title" tone="info">
Reach for a callout when the statement is true of one passage.
</Callout>
<Tip>A tip, written as `<Tip>` rather than `<Callout tone="tip">`.</Tip>Available tones: info, note, success, tip, check, warning, danger.
Cards and tiles
A linked card
With href set, the card renders as an anchor and grows an arrow.
An unlinked card
Without href, the card is a plain titled block.
<CardGroup cols={2}>
<Card title="A linked card" href="/getting-started">
<Icon slot="icon" name="rocket" size={22} />
Given `href` it renders as an anchor and grows an arrow.
</Card>
</CardGroup>Two meanings of cols
Both <Columns> and <CardGroup> take cols, but the prop means different
things for each.
cols means | cols={1} | cols={4} in the reading column | |
|---|---|---|---|
Columns | exactly this many | one full-width cell | four narrow cells |
CardGroup | at most this many | one full-width card | three; the reading column is not wide enough for a fourth ~210px card |
Columns follows the exact layout the author wrote. CardGroup never packs
more than cols, but packs fewer once the column is too narrow for that many
~210px cards — which is the one case where four genuinely becomes three. Two
and three do not have that excuse: asked for two, a group renders two,
whatever the container’s width. Five cards at cols={3} below are the proof,
run rather than described:
One
Two
Three
Four
Five
Three cards, then two: never four, and never one alone stretched to fill the row. Both props take values 1 to 4 and collapse to one column on a phone.
Steps
Numbered automatically
The marker counts steps for you. Pass icon to replace the number. Pass
stepNumber to continue a sequence that started elsewhere.
Any content inside
Including code fences, callouts and tables.
<Steps>
<Step title="Numbered automatically">The marker counts for you.</Step>
<Step title="Any content inside">Including code fences and callouts.</Step>
</Steps>Tabs
Tab groups synchronize when they offer the same labels in the same order. For example, every “npm / pnpm / yarn” group on a site moves together, and the site remembers the choice for the next page. Groups that share only one label do not synchronize. A group offering “REST” alongside two other options is a different group from one offering “REST” alongside four options. Treating them as the same group made unrelated sections jump together.
| Prop | Effect |
|---|---|
sync={false} | Keeps this group local, on Tabs, CodeGroup or RequestExample |
defaultTabIndex={1} | Starts on the second panel. A reader’s remembered choice takes priority over it |
dropdown | On CodeGroup, for when a horizontal language strip would be crowded |
Disclosure
An accordionWith a description line
Closed by default. Pass defaultOpen to start it open. Pass icon for a
glyph beside the label.
A second one
Grouped in an <AccordionGroup>, so the accordions share one border.
<Expandable> is the lighter variant. Use it to reveal a nested object’s
fields inside a parameter list, rather than a section of prose.
Parameter fields
page_idstringrequiredThe page identifier. path, query, header, and body each set where
the parameter lives, and the label reflects the choice.
limitintegerdefault: 20Records per page. The API clamps requests above the maximum instead of rejecting them.
<ParamField path="page_id" type="string" required>
The page identifier.
</ParamField>On an OpenAPI endpoint page you do not write these by hand. The framework generates them from the spec. Write them yourself only for an API this framework does not drive, or for a configuration object.
Badges, updates and tooltips
A badge is filled by default, takes a colour, and can be outlined or carry an icon: Default, green, outlined, and with an icon.
A tooltip wraps inline text.
What changed
<Update> is the changelog entry: a dated block with optional tags, meant
to stack down a release-notes page. The label becomes the anchor, in
slugified form. This gives a date written for a reader a usable permalink
too.
Banners
<Banner> states something true of one page, in the reading column:
For an announcement on every page, set site.banner in
src/docs.config.ts instead. This creates the strip fixed above the navbar,
which this site is using right now. There is deliberately no per-page
equivalent of that strip. A fixed bar states something true of the whole
documentation set. Something true of one page belongs in the page instead.
The site remembers a dismissal against the exact text of the banner. Publishing new wording shows the banner again to everyone who dismissed the previous version. This same behaviour makes the strip unsuitable for anything a reader must not be able to hide.
Calls to action
<CTA> is a block asking the reader to do one thing. The default layout puts
the headline and its supporting line on the left and the action on the right:
<CTA
title="Ready to deploy?"
description="One command puts this site on Cloudflare."
href="/guides/deployment"
label="Deploy it"
/>Three layouts
Each answers a different question about where the block sits.
row is the default and belongs mid-page: it costs about the height of a
paragraph and reads as one line of intent. A mark can sit at its left:
stack centres everything for the end of a page, where the reader has finished
and the block is the next step rather than an aside:
split gives an image half the block, with the text and action in the other
half. Use it when the picture is doing part of the persuading:
This one carries actionWidth="auto", so narrow the window and its button stays
at its own size, flush with the text, rather than stretching. Every other block
on this page keeps the default. See how wide the action
goes for when each is right.
Variants
surface is the default sunken panel, accent fills with the brand colour for
the one call to action on a landing page, and outline is a hairline and
nothing else for a quiet nudge in a long page:
More than one action
Pass buttons to the actions slot instead of using href and label. The
slot takes <Button>, so each action chooses its own weight:
<CTA variant="accent" title="Two ways in" description="…">
<Fragment slot="actions">
<Button href="/getting-started">Get started</Button>
<Button href="/reference/architecture" variant="secondary">How it works</Button>
</Fragment>
</CTA>Buttons on their own
<Button> works outside a CTA. It is always a link: a <button> in static
documentation would need JavaScript to mean anything, and a control that looks
pressable and does nothing is worse than prose. Three variants and three sizes:
How wide the action goes
Once the block stacks (on a phone, or in stack), the action takes the full
width by default, because a 36px pill alone on a narrow line reads as orphaned
rather than as the thing to tap. actionWidth="auto" keeps it at its natural
size, aligned with the text above it, which is right for a block that is an
aside rather than the point:
<CTA actionWidth="auto" title="…" href="/why" label="Read why" />Colours outside the theme
background takes any CSS colour, for a block that has to match something the
theme does not know about.
On a filled block the button is the topbar’s secondary button, --surface
behind --text. Those two invert together with the colour mode, so the pill is
light with dark text on a light page and dark with light text on a dark one,
which a literal grey could not do.
That pairing holds against the theme’s own colours, and cannot hold against one
it has never seen: a fixed background stays put while --surface flips, so a
dark custom fill that reads well in light mode gets a near-black pill on it in
dark mode. Measured on the block above before its action colours were set:
1.26:1, a button you could lose against its own block. So a custom background
is a commitment to actionBackground and actionForeground as well: the same
bargain as foreground, for the same reason.
File trees
app
guides
<Tree>
<TreeFolder name="app">
<TreeFile name="index.mdx" />
</TreeFolder>
</Tree>FileTree, Folder, and File are aliases for the same three components.
Tree.Folder and Tree.File are the same components again, written as member
expressions of their parent. The tree above uses this member-expression form.
All three notations leave identical Markdown in the page’s twin, so an agent
reading it sees a folder listing rather than component syntax.
Colours
<Color.Item> takes either one value or a { light, dark } pair. It captions
the swatch with exactly what you wrote, not a sanitised version of it. Use
variant="table" for a labelled palette. Leave it off for a compact row.
Columns
Columns follows the layout you drew.The right rail
Top-level Panel, RequestExample, and ResponseExample content can occupy the
sticky right rail instead of the reading column. The rightRail field in
frontmatter decides. A panel placed on an ordinary page stays inline, which is
the default rather than a special case:
Page-wide variants
Use <View> for one page-wide language or framework selector. Use <Tabs>
for alternatives local to one passage. Two or more views make the selector
appear. One view on its own renders as an ordinary section. This is what
makes it safe to write a page one view at a time.
With two or more views on a page, a single selector appears above them and switches the whole page at once. This is the TypeScript variant.
This is the Python variant. Only one view is visible at a time. The
selector is the platform’s own <select> element, so an option cannot
carry an icon. This is why a View is labelled by its title alone.
For content that differs between readers and models, rather than between
frameworks, use <Visibility>:
You are reading the rendered page, so this paragraph appears. The Markdown twin of this page carries a different paragraph in its place.
Full explanation in Authoring.
Diagrams
<Mermaid> is the component form of a diagram. A fenced ```mermaid block
is the usual way to write one, and
Code and diagrams covers both. Use the
component for a one-liner, or for the interactive controls:
actions={true} adds pan, zoom, reset, and a fullscreen viewer on hover or
keyboard focus. placement positions the inline controls. Set
actions={false} to make a deliberately static diagram.
Icons
<Icon> places any icon the site knows about, and every icon slot on a
Card or a Tile takes the same names:
<Icon name="rocket" size={20} />Code and diagrams explains where the names come from, and why a page should never use a raw emoji instead.