Quick Start
Prerequisites
- Node.js: ^20.19.0 or >=22.12.0
- VitePress: ^1.6.3
- React/ReactDOM (optional): ^18.2.0
- @vitejs/plugin-react-swc (optional): ^3.9.0
Install Dependencies
pnpm add -D @docs-islands/vitepress @vitejs/plugin-react-swc
pnpm add react react-domConfigure VitePress
Integrate
UIframework compilation support in yourVitePressconfig:ts// .vitepress/config.ts import { defineConfig } from 'vitepress'; import vitepressReactRenderingStrategies from '@docs-islands/vitepress/react'; const vitePressConfig = defineConfig({ // Setup VitePress config... }); // Inject React rendering support and build-time optimizations into Vite vitepressReactRenderingStrategies(vitePressConfig); export default vitePressConfig;Register the framework's corresponding client runtime in theme enhancement:
ts// .vitepress/theme/index.ts import DefaultTheme from 'vitepress/theme'; import reactClientIntegration from '@docs-islands/vitepress/react/client'; import type { Theme } from 'vitepress'; const theme: Theme = { extends: DefaultTheme, async enhanceApp() { await reactClientIntegration(); }, }; export default theme;
Using React Components in Markdown
Write
UIcomponents:tsx// components/Landing.tsx import { useState } from 'react'; export default function Landing() { return <div>Hello World</div>; }Import
UIcomponents inMarkdownand apply directives:md<script lang="react"> import Landing from '../components/Landing'; </script> <Landing ssr:only spa:sr title="Hello" />
Rendering Directives and Behavior
Directive Overview
ssr:only(default)- Pre-render at build time and output static
HTML; no clientHydration. - Best for static content and
SEO‑critical sections; benefitsFCP/LCP/SEOand avoids adding client‑sideJSweight.
- Pre-render at build time and output static
client:load- Pre-render
HTMLand immediatelyHydrationon the client to take over interactivity. - Suited for above‑the‑fold interactive components; can add pressure to TTI.
- Pre-render
client:visible- Pre-render
HTML; performHydrationwhen the component becomes visible. - Suited for offscreen interactive components (comments, charts, etc.); scripts are preloaded by default (not pure lazy).
- Pre-render
client:only- Client‑side rendering only; no SSR/SSG pre-rendering.
- Suited for strong host‑environment dependencies or non‑critical, lightweight widgets.
Directives quick reference
| Directive | Pre-render HTML | Client Hydration | Load timing | Typical usage | spa:sr default |
|---|---|---|---|---|---|
ssr:only | Yes | No | N/A | Static/SEO‑critical sections | Enabled |
client:load | Yes | Immediate | Preload module, hydrate on load | Above‑the‑fold interactive components | Disabled |
client:visible | Yes | On visible | Preload; hydrate on intersection | Offscreen interactions (comments/charts) | Disabled |
client:only | No | N/A | Client‑only | Host‑dependent/lightweight widgets | Disabled |
SPA Synchronous Rendering (spa:sync-render / spa:sr)
During SPA navigations in VitePress, Vue content updates synchronously; however, pre-rendered HTML for non‑Vue components (e.g., React) and their scripts load asynchronously, which can easily cause flicker on weak networks and low-performance devices. spa:sr merges the pre-rendered output of components marked with this directive into the Vue client script, prioritizes blocking download and parsing of all CSS modules for components using the spa:sr directive, and renders synchronously to eliminate flicker.
Default rules:
client:onlycomponents do not supportspa:sr.- Components using
client:*do not enablespa:srby default; explicitly add (spa:sr/spa:sync-render) to enable. - Components using
ssr:only(and components without any directive) enablespa:srby default unless explicitly disabled via (spa:sr:disable/spa:sync-render:disable).
Trade‑off: spa:sr improves navigation smoothness but increases client script size during navigations. Prefer enabling it only for critical rendering components.
Example:
<Landing client:load spa:sr title="Home" />
<Hero ssr:only />
<Chart client:visible />
<Widget client:only />Bundle size note: the size increase applies only to page client scripts loaded during SPA navigations, and does not affect the initial .lean.js used for first‑load hydration in VitePress.
Usage Notes
Component tag naming
- Must start with an uppercase letter (PascalCase style), e.g.
MyComp. - The tag name must exactly match the locally imported name in the same
.mdfile’s<script lang="react">block. If you alias likeimport { Landing as HomeLanding } from '...';, then the tag must be<HomeLanding ... />. - Any mismatch will be skipped at compile time with a warning.
- Must start with an uppercase letter (PascalCase style), e.g.
Self-closing only
Reactcomponents inMarkdownmust be self-closing:<Comp ... />.- Non‑self‑closing forms like
<Comp>...</Comp>are skipped with a warning.
Location and imports
- Components must be imported in the same
Markdownpage inside a<script lang="react">block. Unimported components are ignored. - Components can be used inside
Vueslots/templates (e.g. within<template #default>...</template>); they will still be correctly discovered and transformed.
- Components must be imported in the same
Props passing (initialization)
- All non‑strategy attributes on the tag are passed to the
Reactcomponent as string props.Vuebindings like:page-title="page.title"are evaluated byVuefirst and written asDOMattributes, then forwarded as props duringReactrender/hydration. This is a one‑time data pass, not reactive. - Do not pass functions or event handlers via attributes (e.g.
onClick); bridging callable props/events across frameworks is not supported.
- All non‑strategy attributes on the tag are passed to the
Supported directives
client:only,client:load,client:visible,ssr:only(default).spa:sync-render(akaspa:sr) is disabled by default forclient:*and enabled by default forssr:onlyunless explicitly disabled viaspa:sync-render:disable/spa:sr:disable.
Constraints for Using Node APIs with
ssr:only- A component can only rely on Node APIs (e.g.,
node:fs) if it is rendered exclusively with thessr:onlydirective on a given page. If the same component is also used with anyclient:*directive on the same page, it must not depend on Node APIs. - When using environment APIs like
node:fsto read local files, useimport.meta.dirnameas the base path to resolve the target path.
tsimport { readFileSync } from 'node:fs'; import { join } from 'pathe'; const targetPath = join(import.meta.dirname, 'local-data.json'); const data = JSON.parse(readFileSync(targetPath, 'utf8')) as { data: unknown; };- A component can only rely on Node APIs (e.g.,
Constraint: only static
ESM importis supported inside<script lang="react">. During the initial render,propsare a one‑off snapshot, not a reactive binding (values passed from the parentVuecomponent are used for initialization only).
Troubleshooting (FAQ)
- Tags are ignored: ensure the tag starts with an uppercase letter and exactly matches the local import name; React tags must be self‑closing.
- Nothing renders: the component must be imported in the same
.mdinside<script lang="react">and used outside fenced code blocks. - Flicker on navigation: enable
spa:srfor components critical to above‑the‑fold rendering. - Hydration errors: the runtime falls back to client rendering; verify that server‑rendered markup matches client output and avoid passing functions as attributes.
- Node API usage errors: only use Node APIs when the component is rendered exclusively with
ssr:onlyon that page and resolve paths withimport.meta.dirname.