# 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`:

```mdx title="snippets/upgrade-note.mdx"
<Callout type="note">
  Available since v{props.since}.
</Callout>
```

```mdx title="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:

```jsx title="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>;
}
```

```mdx title="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.

<Callout type="note">
  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.
</Callout>

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

```mdx
<Counter client:visible start={5} />
```

| Directive | Hydrates |
|---|---|
| `client:load` (automatic default) | Immediately on page load |
| `client:idle` | Once the browser is idle |
| `client:visible` | When 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:

```mdx title="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:

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

Both resolve to the exact same file.

<Callout type="tip">
  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).
</Callout>