Skip to main content

Add Interactivity Where It Helps

Relevate supports custom React components as isolated interactive regions inside an otherwise server-rendered page. React loads only on pages that use these components, so ordinary documentation remains lightweight.

Keep component source in snippets/ and import named exports into an MDX page:

docs/
|- guides/
| `- usage.mdx
`- snippets/
`- counter.tsx
snippets/counter.tsx
export const Counter = ({ initial = 0, label = "Count" }) => {
const [count, setCount] = useState(initial)
const doubled = useMemo(() => count * 2, [count])
return (
<button
type="button"
className="rounded-xl bg-primary px-4 py-2 font-semibold text-white"
onClick={() => setCount(count + 1)}
>
{label}: {count} (double {doubled})
</button>
)
}
guides/usage.mdx
import { Counter } from "/snippets/counter"
# Usage
<Counter initial={2} label="Completed tasks" />

Try It Live

These are two separate React islands. Change either value: its state updates without affecting the other island or re-rendering the surrounding documentation page.

Style With Tailwind CSS

Use Tailwind utilities directly in className. Relevate includes imported React snippets in the Tailwind content scan, so utilities used only by a component are generated as part of the documentation build.

<button className="rounded-xl bg-primary px-4 py-2 font-semibold text-white">
Increment
</button>

The live counters above use responsive layout, light and dark variants, disabled states, transitions, and an arbitrary shadow utility. They require no separate stylesheet or Tailwind configuration.

Available React APIs

Component snippets can use React and these hooks without importing packages:

  • useState

  • useEffect

  • useMemo

  • useRef

  • useCallback

  • useReducer

  • useContext

snippets/toggle.tsx
export function Toggle({ left = "Draft", right = "Published" }) {
const [active, setActive] = useState(left)
return (
<div>
{[left, right].map((label) => (
<button key={label} type="button" onClick={() => setActive(label)}>
{label}{active === label ? " ✓" : ""}
</button>
))}
</div>
)
}

Props And Children

Props must be JSON-compatible build-time values:

<Counter initial={3} label="Clicks" enabled />
<Chart labels={["Jan", "Feb", "Mar"]} options={{"compact": true}} />

Supported values include strings, numbers, booleans, null, arrays, and objects. Plain-text children are passed as a children string.

Prop spreads and executable prop expressions are not supported. Pass fixed JSON-compatible values, and use a surrounding personalization condition when the selected component depends on the reader.

Imports And File Rules

React component source must live under /snippets in a .tsx or .jsx file. This keeps executable code separate from authored page content.

  • Export components with named exports.

  • Import those names directly; aliases, namespace imports, and default component imports are rejected.

  • A component may import named exports from other files under /snippets.

  • Package imports and side-effect imports are not supported.

  • Inline component exports in ordinary content pages are rejected.

snippets/status-card.tsx
import { Badge } from "./badge"
export function StatusCard({ status }) {
return <Badge label={status} />
}
import { StatusCard } from "/snippets/status-card"
<StatusCard status="Healthy" />

React Islands And Navigation

Each component instance receives a deterministic island identifier and mounts independently. Relevate unmounts islands removed during client-side navigation and mounts the components required by the next page.

This model keeps the surrounding page server-rendered:

  • Markdown, navigation, metadata, and search remain build-time output.

  • Component source is compiled into a page-specific browser module.

  • React is loaded only when the page declares an island.

  • Multiple instances keep independent React state.

Combine With Personalization

Personalization is still a work in progress. React islands are documented here so projects can prepare their component structure, but do not use personalized branches as production access control yet.

React components can appear inside personalized branches. Every branch is collected during the build, but only the selected branch is sent to the authenticated reader.

import { UsageChart } from "/snippets/usage-chart"
{user.plan === 'enterprise'
? <UsageChart range="30d" />
: <Note>Upgrade to view the interactive usage chart.</Note>}

Reader-specific values cannot be passed directly as dynamic props. Put them in the surrounding expression or render them in a reusable MDX snippet.