Skip to content
Astro as markdown 4 min read

Components in MDX

One of the headline features of MDX is that you can drop real components straight into your prose. Where plain Markdown gives you static text, MDX lets you import .astro components and framework components (React, Vue, Svelte, Solid, Preact) and render them inline alongside your headings and paragraphs. This is how you turn a documentation page into something interactive — embedding a live demo, a callout card, or a chart — without leaving the authoring flow. Crucially, Astro still ships zero JavaScript by default, so you opt into interactivity per component with client:* directives.

Importing components into MDX

Inside an MDX file you import components using standard ESM import statements in the frontmatter area — that is, after the YAML frontmatter but before the content. Once imported, you render the component with JSX-style tags.

---
title: My Post
layout: ../../layouts/PostLayout.astro
---
import Callout from '../../components/Callout.astro';
import Counter from '../../components/Counter.jsx';

# Welcome

This is regular Markdown. Below is an Astro component:

<Callout type="info">Astro components render to static HTML by default.</Callout>

And here is an interactive React island:

<Counter client:load />

Note that the MDX frontmatter is the YAML block delimited by ---. Your import statements go after the closing ---, not inside it.

Astro components in MDX

Astro components are the natural fit because they render to pure HTML at build time and ship no JavaScript. They are ideal for structural and presentational elements such as cards, asides, figures, and layout wrappers.

---
// src/components/Callout.astro
const { type = 'note' } = Astro.props;
---
<aside class={`callout callout-${type}`}>
  <strong>{type.toUpperCase()}</strong>
  <slot />
</aside>

The <slot /> lets you pass MDX content (including more Markdown) as children:

<Callout type="warning">
  Be careful: this operation is **irreversible**.
</Callout>

Tip: Markdown formatting inside a component’s children is only parsed when there is a blank line separating the content from the surrounding tags, mirroring how MDX delimits block content.

Framework components and islands

To use React, Vue, Svelte, Solid, or Preact components, first add the matching integration. Each integration registers the renderer Astro needs to hydrate that framework.

npx astro add react

This updates astro.config.mjs for you:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [mdx(), react()],
});

A framework component imported into MDX renders as static HTML by default — exactly like an Astro component. It only becomes an interactive island when you add a client:* directive.

// src/components/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>;
}
{/* Static — no JS shipped */}
<Counter start={5} />

{/* Interactive — hydrated in the browser */}
<Counter client:load start={5} />

Client directives reference

The client:* directive controls when and whether a component’s JavaScript loads. Choosing the lightest directive that meets your needs keeps the page fast.

DirectiveHydration timingUse for
client:loadImmediately on page loadCritical, above-the-fold interactivity
client:idleWhen the browser is idle (requestIdleCallback)Lower-priority widgets
client:visibleWhen the element scrolls into viewBelow-the-fold demos, charts
client:media={query}When a CSS media query matchesResponsive-only UI (e.g. mobile menus)
client:only={framework}Skips SSR, renders only client-sideComponents that must not render on the server

For client:only, you must name the framework so Astro knows which renderer to use:

<Counter client:only="react" />

Passing props and children

Props pass through JSX attributes. Strings use quotes; everything else uses curly braces for expressions.

<Chart
  title="Monthly revenue"
  data={[10, 24, 18, 30]}
  showLegend={true}
/>

Children passed between tags become the component’s children (React/Preact) or default <slot /> (Astro/Vue/Svelte). You can nest components and Markdown freely.

Overriding default HTML elements

MDX lets you map standard Markdown elements to custom components by passing a components prop where the page is rendered, or by exporting them. A common pattern is replacing <a> or <img> site-wide via a layout that consumes the MDX <Content components={...} /> API.

---
import { Content } from '../content/post.mdx';
import CustomLink from '../components/CustomLink.astro';
---
<Content components={{ a: CustomLink }} />

Best practices

  • Prefer Astro components for static UI — they ship zero JavaScript and keep pages fast.
  • Reach for framework islands only where genuine interactivity is required, and add the integration first with astro add.
  • Pick the lightest client:* directive: default to client:visible or client:idle over client:load when content is below the fold.
  • Always specify the framework for client:only, e.g. client:only="react".
  • Keep import statements after the YAML frontmatter, never inside it.
  • Separate Markdown children from component tags with blank lines so MDX parses the content correctly.
  • Use the components prop to override default elements rather than littering every page with custom tags.
Last updated June 14, 2026
Was this helpful?