Skip to main content
Build interactive elements in your docs using React components and hooks directly in MDX files.

Inline components

Declare components directly in your MDX file:
export const Counter = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

<Counter />

Constraints

React components in Mintlify run in a sandboxed MDX environment with the following constraints:
  • React hooks are pre-injected: useState, useEffect, useRef, useCallback, useMemo, useContext, and useReducer are available without importing them.
  • No external npm packages: Third-party packages (for example, lodash, axios, date-fns) cannot be imported. Use browser built-ins or write the logic inline.
  • No default exports: Use named exports (export const MyComponent = ...). Default exports (export default) are not supported.
  • No cross-snippet imports: Snippet files cannot import other snippet files. Import all dependencies directly in the parent MDX file.
  • No JSON imports: Importing .json files is not supported.

Import components

Component files must be in the /snippets/ folder. Learn more about reusable snippets.
Nested imports are not supported. Import all referenced components directly into the parent MDX file.
Create a component file in snippets/:
/snippets/color-generator.jsx
export const ColorGenerator = () => {
  const [hue, setHue] = useState(180)
  const [saturation, setSaturation] = useState(50)
  const [lightness, setLightness] = useState(50)
  const [colors, setColors] = useState([])

  useEffect(() => {
    const newColors = []
    for (let i = 0; i < 5; i++) {
      const l = Math.max(10, Math.min(90, lightness - 20 + i * 10))
      newColors.push(`hsl(${hue}, ${saturation}%, ${l}%)`)
    }
    setColors(newColors)
  }, [hue, saturation, lightness])

  const copyToClipboard = (color) => {
    navigator.clipboard
      .writeText(color)
      .then(() => {
        console.log(`Copied ${color} to clipboard!`)
      })
      .catch((err) => {
        console.error("Failed to copy: ", err)
      })
  }

  return (
    <div className="p-4 border dark:border-zinc-950/80 rounded-xl not-prose">
      <div className="space-y-4">
        <div className="space-y-2">
          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Hue: {hue}°
            <input
              type="range"
              min="0"
              max="360"
              value={hue}
              onChange={(e) => setHue(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(0, ${saturation}%, ${lightness}%), 
                  hsl(60, ${saturation}%, ${lightness}%), 
                  hsl(120, ${saturation}%, ${lightness}%), 
                  hsl(180, ${saturation}%, ${lightness}%), 
                  hsl(240, ${saturation}%, ${lightness}%), 
                  hsl(300, ${saturation}%, ${lightness}%), 
                  hsl(360, ${saturation}%, ${lightness}%))`,
              }}
            />
          </label>

          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Saturation: {saturation}%
            <input
              type="range"
              min="0"
              max="100"
              value={saturation}
              onChange={(e) => setSaturation(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(${hue}, 0%, ${lightness}%), 
                  hsl(${hue}, 50%, ${lightness}%), 
                  hsl(${hue}, 100%, ${lightness}%))`,
              }}
            />
          </label>

          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Lightness: {lightness}%
            <input
              type="range"
              min="0"
              max="100"
              value={lightness}
              onChange={(e) => setLightness(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(${hue}, ${saturation}%, 0%), 
                  hsl(${hue}, ${saturation}%, 50%), 
                  hsl(${hue}, ${saturation}%, 100%))`,
              }}
            />
          </label>
        </div>

        <div className="flex space-x-1">
          {colors.map((color, idx) => (
            <div
              key={idx}
              className="h-16 rounded flex-1 cursor-pointer transition-transform hover:scale-105"
              style={{ backgroundColor: color }}
              title={`Click to copy: ${color}`}
              onClick={() => copyToClipboard(color)}
            />
          ))}
        </div>

        <div className="text-sm font-mono text-zinc-950/70 dark:text-white/70">
          <p>
            Base color: hsl({hue}, {saturation}%, {lightness}%)
          </p>
        </div>
      </div>
    </div>
  )
}
Then import and use it:
import { ColorGenerator } from "/snippets/color-generator.jsx"

<ColorGenerator />

Considerations

  • SEO: Search engines may not fully index client-rendered dynamic content.
  • Initial load: Visitors may see a flash before components render.
  • Accessibility: Ensure screen readers announce dynamic content changes.
  • Optimize dependency arrays: Only include necessary dependencies in useEffect.
  • Memoize expensive operations: Use useMemo or useCallback where appropriate.
  • Reduce re-renders: Break large components into smaller ones.
  • Lazy loading: Defer rendering complex components to improve initial page load. Because the MDX sandbox does not support React.lazy or dynamic import(), gate heavy components behind user interaction or visibility instead. See Defer rendering for heavy components.

Defer rendering for heavy components

React.lazy, Suspense, and dynamic import() are not available in the MDX sandbox. To get the same benefit, render a lightweight placeholder first and only mount the expensive component after the reader opts in. This keeps the initial page load fast while still letting readers interact with the full component. The example below keeps the ColorGenerator from the previous section unmounted until the reader clicks Load color generator:
import { ColorGenerator } from "/snippets/color-generator.jsx"

export const LazyColorGenerator = () => {
  const [show, setShow] = useState(false)

  if (!show) {
    return (
      <button onClick={() => setShow(true)}>
        Load color generator
      </button>
    )
  }

  return <ColorGenerator />
}

<LazyColorGenerator />
To defer rendering until the component scrolls into view, swap the button for an IntersectionObserver set up inside a useEffect. The pattern is the same: keep show false until the trigger fires, then return the heavy component.