# React, Vue and MDX JavaScript only where you ask for it.

> How to use React, Vue and MDX in vagejs. Layouts as components, pages as MDX, and only the components you mark interactive send JavaScript.

Since 0.10.0, you can write layouts as React or Vue components and pages as MDX. vagejs turns them into plain HTML when it builds the site, so pages send no JavaScript. Since 0.11.0, a React component can also run in the browser when you mark it [interactive](#interactive-components).

Updated 26 October 2026

You install only what you use. Sites that don't use components stay exactly as they are. For a full worked example, see [how to add an interactive React component to a static site](https://vagejs.com/guides/interactive-react-component-static-site/), and for an overview, [React, Vue and MDX in vagejs](https://vagejs.com/features/react-vue-mdx/).

## React layouts

Install React:

terminal

```
npm install react react-dom
```

Then write a layout as `src/layouts/default.tsx`. It gets the same values as any layout (`content`, `page`, `site`, `pages`, `menus`):

```
import type { LayoutProps } from "vagejs";

export default function Layout({ content, page, site }: LayoutProps) {
  return (
    <html lang={page.lang}>
      <head>
        <meta charSet="utf-8" />
        <title>{`${page.title} | ${site.title}`}</title>
      </head>
      <body>
        <main dangerouslySetInnerHTML={{ __html: content }} />
      </body>
    </html>
  );
}
```

`content` is your page, already turned into safe HTML, so insert it with `dangerouslySetInnerHTML`. React escapes everything else. vagejs still adds the description, canonical link, Open Graph tags and structured data to the `<head>`.

## Vue layouts

Install Vue and its Vite plugin:

terminal

```
npm install vue
npm install --save-dev @vitejs/plugin-vue
```

Then write `src/layouts/default.vue`. Declare the values you use with `defineProps`, and insert the page with `v-html`:

```
<script setup lang="ts">
defineProps<{ content: string; page: { title: string; lang: string } }>();
</script>

<template>
  <html :lang="page.lang">
    <head><title>{{ page.title }}</title></head>
    <body><main v-html="content"></main></body>
  </html>
</template>
```

## MDX pages

MDX is Markdown that can use components. Install it with React:

terminal

```
npm install react react-dom
npm install --save-dev @mdx-js/mdx
```

Then write a page ending in `.mdx`, such as `src/pages/pricing.mdx`. It has the same frontmatter as any page, and can import React components from `src/components/`:

```
---
title: Pricing
description: Plans and prices.
---
import PlanCard from "../components/PlanCard.tsx";

# Pricing

<PlanCard name="Team" price="$20 a month" />
```

MDX pages get the same addresses, head tags, sitemap entries and checks as Markdown pages. Components inside MDX must be React components. Vue components work in layouts only.

## Interactive components

Most components only need to become HTML. When one needs to respond to visitors, such as a counter, a filter or a calculator, mark it with a `client:` directive where you use it:

```
import Calculator from "../components/Calculator.tsx";

<Calculator client:load rate={4.5} />
```

| Directive | Starts |
| --- | --- |
| `client:load` | as soon as the page loads |
| `client:visible` | when it scrolls into view |
| `client:idle` | when the browser has nothing else to do |

The component is still turned into HTML first, so visitors see it straight away and the page works before the JavaScript arrives. vagejs then sends the component's code and React to pages that use it, and adds one small loader script. Pages without interactive components still send none.

Rules:

-   The component must be a React function component exported from a file in `src/components/`.
-   Its props must be plain data: text, numbers, true or false, lists and objects. Functions can't be sent to the browser.
-   It can't have content between its tags yet. Pass text as a prop instead.
-   It can't contain another interactive component. Mark only the outermost one.
-   Files it imports other than JavaScript, such as CSS or images, aren't published. Use paths to files in `public/` instead.
-   It works in MDX pages and React layouts. Vue components can't be interactive yet.

React itself is about 215 KB of JavaScript. That's more than the default 100 KB budget in `vage audit`, which counts it, so raise `performance.budgets.javascriptKb` if you accept the cost. When plain HTML can do the job (a `<details>` element, a link, a form), it's lighter and works everywhere.

## Things to know

-   **Only marked components run in the browser.** Everything else is turned into HTML once, when the site is built. In those, `useState`, effects and `onClick` do nothing.
-   **MDX pages are code.** An `.mdx` page can import and run anything when the site is built, just like a layout. vagejs doesn't clean it up the way it cleans HTML in `.md` pages. Use MDX only for content you'd trust in a layout, and keep other writers on `.md`. `vage check` still checks what MDX pages produce: links, the link lock, hidden links and SEO.
-   **Styles go in `public/`.** CSS inside components (a Vue `<style>` block, or `import "./card.css"`) isn't added to the site. Put your CSS in `public/` and link it from the layout.
-   **Your own structured data.** vagejs adds JSON-LD for you. If a component layout writes its own, insert it with `dangerouslySetInnerHTML` (React) or `v-html` (Vue): Vue escapes the quotes in a plain `<script>` in a template, which breaks the JSON.
-   **`404.mdx` works** as the page shown for addresses that don't exist, like `404.md`.
-   **The editor lists `.md` pages only.** Edit `.mdx` pages in your code editor.
-   **Missing packages are named.** If a site uses a `.vue` layout or an `.mdx` page without the package installed, the build stops and prints the install command.
