Skip to main content
Back to Blog frontend

Example MDX with components

Tom Hermans

Table Of Contents

MDX with Astro components gives you the best of both worlds: the simplicity of markdown for writing and the flexibility of components for rich, interactive content. Start simple with components like TwoColumns and Callout, then build more complex ones as your needs grow.

The web is a flexible enough domain that I think it belongs in the realm of architecture. A city where all buildings look alike has a soul-crushing quality about it. The same is true, I think, of the web..

Two Columns example

Left Column

This is the left column content. You can use:

  • Markdown formatting
  • Bold and italic text
  • Lists
  • Code blocks
const example = 'code';

Right Column

This is the right column content. You can also use:

  1. Numbered lists
  2. Links like this
  3. Images
  4. Any markdown!

Even blockquotes work here.

And here’s regular content after the columns that goes full-width again.

Another Section

You can even customize the gap between columns by passing a gap prop! Here it’s set at 3rem.

The columns automatically stack on mobile devices (screens under 768px).

Using Astro Components in MDX Blog Posts

What is MDX?

MDX is markdown with the ability to import and use components. It combines the simplicity of markdown with the power of React-like components, making it perfect for blog posts that need more than basic formatting.

Converting Markdown to MDX

Simply rename your file from .md to .mdx:

blog-post.md  →  blog-post.mdx

That’s it! Your existing markdown will still work, but now you can also import components.

Importing Components

Understanding Import Paths

When importing components in MDX, you need to specify the correct relative path from your blog post to the component:

If your structure is:

src/
  components/
    TwoColumns.astro
  content/
    blog/
      my-post.mdx

Your import should be:

import TwoColumns from '../../../components/TwoColumns.astro';

Path breakdown:

  • ../ - goes up from blog/ to content/
  • ../../ - goes up from content/ to src/
  • ../../../components/ - enters the components/ folder

Alternative: Path Aliases

If your tsconfig.json has path aliases configured:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

You can use cleaner imports:

import TwoColumns from '@/components/TwoColumns.astro';

Using Components in MDX

Basic Usage

---
title: "My Blog Post"
---

import TwoColumns from '@/components/TwoColumns.astro';

# My Post Title

Regular markdown content here.

<TwoColumns>
  <div slot="left">
    Left column content
  </div>
  <div slot="right">
    Right column content
  </div>
</TwoColumns>

More markdown content continues...

Named Slots

Astro components use named slots to organize content:

<TwoColumns>
  <div slot="left">...</div>
  <div slot="right">...</div>
</TwoColumns>

The slot attribute tells the component where to place that content.

Passing Props

You can pass properties to components:

<TwoColumns gap="3rem" class="custom-style">
  ...
</TwoColumns>

Nesting Markdown

You can use full markdown syntax inside component slots:

<TwoColumns>
  <div slot="right">
    ## Heading
    
    Regular **markdown** works here:
    - Lists
    - Links
    - Code blocks
    
    ``js
    const code = 'examples';
    ``
  </div>
  <div slot="right">
    More markdown here!
  </div>
</TwoColumns>

Useful Component Ideas

1. Callout/Alert Box

Component: Callout.astro

---
interface Props {
  type?: 'info' | 'warning' | 'success' | 'error';
  title?: string;
}

const { type = 'info', title } = Astro.props;
---

<div class={`callout callout-${type}`}>
  {title && <div class="callout-title">{title}</div>}
  <div class="callout-content">
    <slot />
  </div>
</div>

<style>
  .callout {
    padding: 1.5rem;
    margin: 1.5rem 0;
    border-left: 4px solid;
    border-radius: 4px;
  }
  .callout-info { 
    background: #e3f2fd; 
    border-color: #2196f3; 
  }
  .callout-warning { 
    background: #fff3e0; 
    border-color: #ff9800; 
  }
  .callout-success { 
    background: #e8f5e9; 
    border-color: #4caf50; 
  }
  .callout-error { 
    background: #ffebee; 
    border-color: #f44336; 
  }
  .callout-title {
    font-weight: bold;
    margin-bottom: 0.5rem;
  }
</style>

Usage:

<Callout type="warning" title="Important">
  Make sure to backup your data before proceeding!
</Callout>

2. Code Comparison

Component: CodeCompare.astro

---
interface Props {
  before?: string;
  after?: string;
}

const { before = 'Before', after = 'After' } = Astro.props;
---

<div class="code-compare">
  <div class="compare-side">
    <h4>{before}</h4>
    <slot name="before" />
  </div>
  <div class="compare-side">
    <h4>{after}</h4>
    <slot name="after" />
  </div>
</div>

<style>
  .code-compare {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
    margin: 2rem 0;
  }
  .compare-side h4 {
    margin-top: 0;
    color: #666;
  }
  @media (max-width: 768px) {
    .code-compare {
      grid-template-columns: 1fr;
    }
  }
</style>

Usage:

<CodeCompare before="Old Way" after="New Way">
  <div slot="before">
    ``js
    const x = function() {
      return 'old';
    }
    ``
  </div>
  <div slot="after">
    ``js
    const x = () => 'new';
    ``
  </div>
</CodeCompare>

3. Image with Caption

Component: Figure.astro

---
interface Props {
  src: string;
  alt: string;
  caption?: string;
}

const { src, alt, caption } = Astro.props;
---

<figure>
  <img src={src} alt={alt} />
  {caption && <figcaption>{caption}</figcaption>}
</figure>

<style>
  figure {
    margin: 2rem 0;
    text-align: center;
  }
  img {
    max-width: 100%;
    height: auto;
    border-radius: 8px;
  }
  figcaption {
    margin-top: 0.5rem;
    font-style: italic;
    color: #666;
    font-size: 0.9rem;
  }
</style>

Usage:

<Figure 
  src="/images/example.jpg" 
  alt="Example diagram"
  caption="Figure 1: This shows the component architecture"
/>

4. Expandable Section

Component: Details.astro

---
interface Props {
  summary: string;
  open?: boolean;
}

const { summary, open = false } = Astro.props;
---

<details open={open}>
  <summary>{summary}</summary>
  <div class="details-content">
    <slot />
  </div>
</details>

<style>
  details {
    margin: 1.5rem 0;
    padding: 1rem;
    border: 1px solid #e0e0e0;
    border-radius: 4px;
  }
  summary {
    cursor: pointer;
    font-weight: bold;
    user-select: none;
  }
  summary:hover {
    color: #2196f3;
  }
  .details-content {
    margin-top: 1rem;
  }
</style>

Usage:

<Details summary="Click to see advanced options">
  Here are some advanced configuration options you might need...
</Details>

5. Side-by-Side Images

Component: ImageGrid.astro

---
interface Props {
  columns?: number;
  gap?: string;
}

const { columns = 2, gap = '1rem' } = Astro.props;
---

<div class="image-grid" style={`--columns: ${columns}; --gap: ${gap}`}>
  <slot />
</div>

<style>
  .image-grid {
    display: grid;
    grid-template-columns: repeat(var(--columns, 2), 1fr);
    gap: var(--gap, 1rem);
    margin: 2rem 0;
  }
  .image-grid :global(img) {
    width: 100%;
    height: auto;
    border-radius: 8px;
  }
  @media (max-width: 768px) {
    .image-grid {
      grid-template-columns: 1fr;
    }
  }
</style>

Usage:

<ImageGrid columns={3} gap="2rem">
  ![Image 1](/img1.jpg)
  ![Image 2](/img2.jpg)
  ![Image 3](/img3.jpg)
</ImageGrid>

6. YouTube Embed

Component: YouTube.astro

---
interface Props {
  id: string;
  title?: string;
}

const { id, title = 'YouTube video' } = Astro.props;
---

<div class="video-container">
  <iframe
    src={`https://www.youtube.com/embed/${id}`}
    title={title}
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen
  ></iframe>
</div>

<style>
  .video-container {
    position: relative;
    padding-bottom: 56.25%; /* 16:9 aspect ratio */
    height: 0;
    overflow: hidden;
    margin: 2rem 0;
  }
  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border-radius: 8px;
  }
</style>

Usage:

<YouTube id="dQw4w9WgXcQ" title="Example Video" />

Best Practices

1. Keep Components Reusable

Design components that can be used across multiple blog posts, not just for one specific case.

2. Make Components Responsive

Always include mobile breakpoints in your component styles.

3. Provide Sensible Defaults

Use default prop values so components work with minimal configuration.

4. Document Your Components

Add comments explaining what props are available and how to use them.

5. Use Semantic HTML

Choose appropriate HTML elements (figure, details, aside, etc.) for better accessibility.

6. Style Isolation

Use scoped styles in Astro components to avoid CSS conflicts.

Troubleshooting

Component Not Found

  • Check your import path is correct
  • Verify the component file exists
  • Make sure you’re using .mdx not .md

Markdown Not Rendering Inside Component

  • Ensure you have blank lines around markdown content
  • Check that slots are properly defined in the component

Styles Not Applying

  • Verify <style> tags are in the Astro component
  • Check for CSS specificity conflicts
  • Use :global() for styling slotted content if needed

Conclusion

MDX with Astro components gives you the best of both worlds: the simplicity of markdown for writing and the flexibility of components for rich, interactive content. Start simple with components like TwoColumns and Callout, then build more complex ones as your needs grow.

Back to Blog

Let's Talk

Tom avatar

Get in touch

I work at the intersection of design and code.
Interested? Hit me up.
tomhermans@gmail.com

Copyright © 2026 Tom Hermans. Made by Tom Hermans .

All rights reserved 2026 inc Tom Hermans