WritedocsWritedocs

Snippets

A snippet is a file you write once and import into as many pages as you want — either a chunk of MDX prose, or a real React component. Both live in a snippets/ folder next to docs/ and writedocs.json:

my-docs/
├── writedocs.json
├── index.mdx
├── docs/
│   └── getting-started.mdx
└── snippets/
    ├── upgrade-note.mdx
    └── Counter.jsx

snippets/ isn’t scanned for pages the way docs/ is — nothing in it appears in the sidebar or gets its own URL. It’s purely a place to put things other pages import.

MDX snippets

An MDX snippet is just an .mdx file with no frontmatter — import its default export and use it like any other component. Props you pass are available inside the snippet via props:

snippets/upgrade-note.mdx
<Callout type="note">
  Available since v{props.since}.
</Callout>
docs/getting-started.mdx
import UpgradeNote from '../snippets/upgrade-note.mdx';

<UpgradeNote since="2.0" />

Notice upgrade-note.mdx uses <Callout> with no import of its own — Writedocs’ built-in components (Callout, Card, Tabs, …) are available inside a snippet exactly the same way they’re available on an ordinary page: just write the tag.

React component snippets

A snippet can also be a real .jsx or .tsx file — a genuine React component, hooks included:

snippets/Counter.jsx
import { useState } from 'react';

export default function Counter({ start = 0 }) {
  const [count, setCount] = useState(start);
  return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}
docs/getting-started.mdx
import Counter from '../snippets/Counter.jsx';

<Counter start={5} />

That’s the whole thing — no extra directive needed. Every usage of a component imported from a .jsx/.tsx file hydrates automatically, so useState, event handlers, and everything else just work.

Under the hood, Astro only ships a framework component’s JavaScript to the browser where it’s used with a client:* directive (client:load, client:visible, …) — without one, it still renders fine, just as inert static HTML. Writedocs adds client:load automatically to every snippet usage that doesn’t already have one of its own, so this is never something you need to know or write. It only matters if you want to change the strategy — see below.

Writing a client:* attribute explicitly still works, and overrides the automatic client:load:

<Counter client:visible start={5} />
DirectiveHydrates
client:load (automatic default)Immediately on page load
client:idleOnce the browser is idle
client:visibleWhen the component scrolls into view

This applies per usage, not per snippet file — the same Counter can hydrate immediately in one place and wait until scrolled into view somewhere else.

A usage that passes a component as a prop — <Playground Renderer={MyBlock} /> — is the one exception: it renders, but isn’t hydrated. A component can’t be sent to the browser as a prop, so hydrating it would only break the page.

Hooks without imports

useState, useEffect, useRef, useCallback, useMemo, useContext, and useReducer work in a snippet or page even without import { ... } from 'react' — writedocs adds the import when a file calls one. This is how Mintlify content is written, so snippets copied from a Mintlify project work as they are.

Components defined in a page

A page can define a React component itself and use it right away:

docs/getting-started.mdx
export const Counter = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
};

<Counter />

A page component that calls a hook, or that’s passed as a prop to a snippet component, is moved into a snippet file of its own when the site builds, so it hydrates like any other snippet. Components writedocs provides (Card, Icon, …) are built for pages, not React — inside such a component they render just their content, Icon renders nothing, and CodeBlock renders as a plain, unhighlighted code block. writedocs validate and the build warn about each one, with its file and line. For full control, write the component as a snippet file with your own markup.

Import paths

Snippets can be imported by relative path, or with a leading / rooted at the content directory itself (not the filesystem) — handy from a deeply nested page, since it reads the same regardless of how many folders deep the importing page is:

import UpgradeNote from '../../../snippets/upgrade-note.mdx';
import UpgradeNote from '/snippets/upgrade-note.mdx';

Both resolve to the exact same file.

See docs.json-examples/00-kitchen-sink/ in the Writedocs repo (snippets/, imported from docs/core/2026-01/guides/introduction.mdx) for a complete, buildable example covering both snippet types (an MDX snippet and a React component with hooks) and both import styles (relative and the /snippets/... alias).