Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 13

Styling MDX with Custom Components

Recommended resources

Hello! Welcome to the final lesson in our module on content authoring.

In our last session, you mastered how to query and filter your content collections, which is essential for building pages like a blog index. You learned to use getCollection to fetch a list of your MDX files and display their metadata. Now, we need to focus on how the content inside those files is rendered. When you render an MDX file, Astro converts the Markdown syntax into standard HTML elements (<h1>, <p>, <blockquote>, etc.). While functional, this default output is visually plain and may not align with the "premium, minimal, and trustworthy" design you're aiming for with your accounting website.

Today, we'll address this by learning how to customize the styling of MDX content by mapping HTML elements to custom components. This powerful technique gives you complete control over your content's appearance, allowing you to enforce a consistent and branded design system across all your blog posts, service pages, and case studies.

The Problem: Default Markdown Styling

When Astro's MDX integration processes a file, it converts Markdown into HTML. For example:

  • # My Title becomes <h1>My Title</h1>
  • > A quote. becomes <blockquote><p>A quote.</p></blockquote>
  • ![alt text](./image.png) becomes <img src="..." alt="alt text">

The browser then applies its default styles to these elements, which are rarely what you want for a polished website. To achieve a custom look, you need a way to intercept this process and substitute your own styled components.


The Solution: The components Prop

Astro provides an elegant solution for this. When you render MDX content using the <Content /> component (which you get from post.render()), you can pass a components prop. This prop is an object that maps standard HTML tag names to the custom components you want to use instead.

Let's walk through a practical example. A common element to customize is the blockquote.

1. Create a Custom Blockquote Component

First, create a new Astro component that will define the custom look for your blockquotes. This component will use the <slot /> element to render the content passed into it.

Create the file src/components/content/StyledBlockquote.astro:

{/* src/components/content/StyledBlockquote.astro */}
---
---
<blockquote class="custom-blockquote">
  <slot />
</blockquote>

<style>
  .custom-blockquote {
    border-left: 3px solid #005f73; /* A professional, trustworthy color */
    background-color: #f8f9fa;
    padding: 1rem 1.5rem;
    margin: 1.5rem 0;
    font-style: italic;
    color: #343a40;
    border-radius: 4px;
  }
</style>

This component wraps the blockquote content and applies specific styles that feel more intentional and aligned with a professional aesthetic.

2. Applying the Custom Component

Now, in the template where you render your MDX content (which, as you'll see in the next module, will be a dynamic page like src/pages/blog/[slug].astro), you import your new component and pass it to <Content />.

{/* Example of a page rendering a blog post */}
---
// Import the custom component
import StyledBlockquote from '../../components/content/StyledBlockquote.astro';

// Assume 'post' is an entry from a getCollection() query
const { Content } = await post.render();
---

<article>
  {/* The magic happens here! */}
  <Content components={{ blockquote: StyledBlockquote }} />
</article>

With this change, every time your MDX file uses the > syntax for a blockquote, Astro will automatically render your StyledBlockquote.astro component instead of the default <blockquote> tag.


Taking Full Control of Your Content's Style

This technique can be applied to any standard HTML element that Markdown generates. The Astro documentation provides a clear overview of this feature.

astrojs/mdx - Astro Docs

The official Astro documentation explains how to assign custom components to HTML elements and how to pass them when rendering content from collections. This will solidify your understanding of the core mechanism.

Please read the sections titled 'Assigning Custom Components to HTML elements' and 'Passing components to MDX content'. Focus on the example of passing a components object to the <Content /> component when rendering collection entries.


A More Advanced Example: Custom Image Components

One of the limitations of standard Markdown is that an image is just an image. You can't easily add a caption, wrap it in a <figure> element for better semantics, or apply complex styling. This is a perfect use case for a custom component.

The following blog post provides an excellent, practical walkthrough of this exact scenario.

Assigning custom components to HTML elements in Astro

This article by Dzmitry Kozhukh demonstrates a powerful, real-world use case: creating a custom image component to automatically add captions. This is a fantastic technique for building a professional blog.

Read 'The problem' to understand the limitation of default Markdown images, then read 'The solution' to see how mapping the img tag to a custom Astro component solves it elegantly.

Inspired by that article, let's create our own ArticleImage.astro component. This component will not only wrap the image in a <figure> but also cleverly use the image's alt text as a <figcaption>.

Create the file src/components/content/ArticleImage.astro:

{/* src/components/content/ArticleImage.astro */}
---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';

interface Props {
  src: string | ImageMetadata;
  alt: string;
}

const { src, alt } = Astro.props;
---
<figure class="article-image">
  {/* We use Astro's <Image /> component for automatic optimization */}
  <Image 
    src={src} 
    alt={alt} 
    widths={[400, 800, 1200]} 
    sizes="(max-width: 800px) 100vw, 800px" 
  />
  
  {/* If alt text exists, render it as a caption */}
  {alt && <figcaption>{alt}</figcaption>}
</figure>

<style>
  .article-image {
    margin: 2.5rem 0;
  }
  .article-image img {
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.05);
  }
  .article-image figcaption {
    text-align: center;
    font-size: 0.9rem;
    color: #6c757d; /* A muted gray for secondary text */
    margin-top: 0.75rem;
    font-style: italic;
  }
</style>

Note: We're using Astro's built-in <Image /> component here, which automatically handles optimization. You'll learn more about this in our module on performance, but for now, know that it's the best practice for handling images.

Now, you can add this to your components object:

---
import StyledBlockquote from '../../components/content/StyledBlockquote.astro';
import ArticleImage from '../../components/content/ArticleImage.astro';

const { Content } = await post.render();
---

<article>
  <Content components={{ 
    blockquote: StyledBlockquote,
    img: ArticleImage 
  }} />
</article>

With this setup, every image in your MDX files, written as ![A photo of our team working](./team.jpg), will be rendered as a fully-styled, accessible, and optimized figure with a caption.


Architecting for Maintainability

As you add more custom components for headings, links, lists, etc., passing them all in the page template can get messy. Since you value clean architecture, a better approach is to centralize this mapping.

Create a single file to manage all your MDX component overrides.

Create src/components/mdxComponents.js:

// src/components/mdxComponents.js
import StyledBlockquote from './content/StyledBlockquote.astro';
import ArticleImage from './content/ArticleImage.astro';

// You can add more custom components here as you create them
// import CustomH2 from './content/CustomH2.astro';
// import CustomLink from './content/CustomLink.astro';

export const mdxComponents = {
  blockquote: StyledBlockquote,
  img: ArticleImage,
  // h2: CustomH2,
  // a: CustomLink,
};

Now, your page template becomes much cleaner and easier to maintain:

---
import { mdxComponents } from '../../components/mdxComponents';
const { Content } = await post.render();
---

<article>
  <Content components={mdxComponents} />
</article>

This approach keeps your styling logic separate from your page logic, making your project scalable and easy to update.

Test your understanding!

Your task is to create a custom component for all <h2> headings in your MDX content. For a professional accounting site, you want subheadings to be clear and visually distinct.

  1. Create a new component named Heading2.astro inside src/components/content/.
  2. Style it with a font-weight of 600, a font-size of 1.5rem, and a 1px solid #dee2e6 bottom border with some padding.
  3. Update the src/components/mdxComponents.js file to map the h2 HTML element to your new Heading2.astro component.
Show answer

1. Create src/components/content/Heading2.astro:

---
---
<h2 class="custom-h2">
  <slot />
</h2>

<style>
  .custom-h2 {
    font-size: 1.5rem; /* 24px */
    font-weight: 600;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid #dee2e6;
    margin-top: 2.5rem;
    margin-bottom: 1.5rem;
  }
</style>

2. Update src/components/mdxComponents.js:

// src/components/mdxComponents.js
import StyledBlockquote from './content/StyledBlockquote.astro';
import ArticleImage from './content/ArticleImage.astro';
import Heading2 from './content/Heading2.astro'; // Import the new component

export const mdxComponents = {
  blockquote: StyledBlockquote,
  img: ArticleImage,
  h2: Heading2, // Add the new mapping
};

Now, whenever you use ## A Subheading in your MDX files, it will be automatically rendered with these specific styles, ensuring consistency across your entire site.


Conclusion

Congratulations on completing the module on Content Authoring! You have now mastered a critical skill for building a bespoke, content-driven website. By mapping Markdown syntax to custom components, you've unlocked the ability to create a truly unique and professional reading experience that goes far beyond default styles.

Key Takeaways:

  • The <Content components={...} /> prop is the key to overriding default HTML rendering in MDX.
  • You can map any Markdown-generated HTML tag (like h1, p, img, blockquote) to a custom Astro or UI framework component.
  • This technique is invaluable for creating a consistent design system and overcoming the limitations of standard Markdown, enabling features like styled image captions.
  • Centralizing your component mappings in a dedicated file (mdxComponents.js) is a clean architectural pattern that enhances maintainability.

Up Next:
You now have a powerful system for authoring, querying, and styling content. However, your blog posts and service pages still don't have their own URLs. In our next module, Dynamic Routing and Layouts, we will tackle this head-on. You will first learn to create a base layout to provide a consistent header, footer, and structure for your entire site. Then, you will dive into one of Astro's most powerful features, getStaticPaths, to programmatically generate a unique, static page for every single entry in your content collections.