Custom Components in Markdown and MDX for Astro Starlight

A practical guide to building reusable Astro components and using them inside Starlight documentation pages, lessons, world bible entries, character sheets, and reference pages.

What You Are Learning

This tutorial teaches how custom components work in an Astro Starlight site, especially when writing content in Markdown and MDX.

You will learn:

The Big Picture

Markdown is excellent for writing simple content. MDX is useful when your content needs reusable visual pieces.

Markdown page
  ↓
Good for normal writing
  ↓
Headings, paragraphs, links, lists, images, code blocks
MDX page
  ↓
Good for writing plus components
  ↓
Markdown content + imported Astro components

Think of Markdown as a clean notebook. Think of MDX as a notebook where you can insert reusable widgets, cards, panels, diagrams, and layout blocks.

What Is a Component?

A component is a reusable piece of a webpage. Instead of rewriting the same HTML over and over, you create one component and reuse it many times.

For example, instead of manually writing a styled note box on every page, you can create one InfoBox component.

<InfoBox title="Remember">
Markdown is for writing. MDX is for writing plus components.
</InfoBox>

The component controls the structure and styling. The MDX page supplies the content.

Markdown vs MDX for Components

File Type Can Use Normal Markdown? Can Import Components? Best Use
.md Yes Usually no Simple articles, lessons, reference pages
.mdx Yes Yes Pages with reusable cards, panels, examples, widgets, or custom layouts
.astro Not as normal content Yes Full page templates, layouts, and reusable UI pieces

Simple rule:

If the page is mostly writing, use .md.
If the page needs components, use .mdx.
If you are building the component itself, use .astro.

Where Components Go

Reusable components usually live in:

src/components/

Example structure:

src/
  components/
    InfoBox.astro
    CharacterStats.astro
    LessonObjectives.astro
    FactionCard.astro
    LocationCard.astro
  content/
    docs/
      lessons/
        custom-components.mdx
      characters/
        example-character.mdx

The component lives in src/components/. The MDX page imports and uses it.

Your First Component: InfoBox

Create this file:

src/components/InfoBox.astro

Add this code:

---
const { title = "Note" } = Astro.props;
---

<aside class="info-box">
  <h2>{title}</h2>
  <div>
    <slot />
  </div>
</aside>

This component has two important parts:

Use the Component in MDX

Create an MDX page:

src/content/docs/lessons/custom-components.mdx

Add this content:

---
title: "Custom Components"
description: "Using reusable Astro components inside MDX."
---

import InfoBox from "../../../components/InfoBox.astro";

# Custom Components

<InfoBox title="Remember">
Components help you reuse the same layout or visual pattern across many pages.
</InfoBox>

The import path may change depending on where your MDX file is located. Count the folders carefully.

Understanding Import Paths

An import path tells the MDX file where to find the component.

import InfoBox from "../../../components/InfoBox.astro";

If your MDX file is here:

src/content/docs/lessons/custom-components.mdx

And your component is here:

src/components/InfoBox.astro

You may need to go upward through folders using ../.

lessons/custom-components.mdx
  up to docs
  up to content
  up to src
  then into components/InfoBox.astro

That is why the path may look like:

../../../components/InfoBox.astro

Props: Passing Data into Components

Props are values passed into a component. In this example, title is a prop:

<InfoBox title="Important">
This is important content.
</InfoBox>

The component receives the prop like this:

---
const { title = "Note" } = Astro.props;
---

The default value "Note" is used if no title is provided.

Slots: Passing Content into Components

A slot is a placeholder for nested content.

<InfoBox title="Tip">
This text goes into the slot.
</InfoBox>

The component decides where that nested content appears:

<aside class="info-box">
  <h2>{title}</h2>
  <div>
    <slot />
  </div>
</aside>

Slots are useful for callout boxes, cards, lesson objectives, warnings, summaries, and reusable page sections.

Add Custom CSS

Create a custom CSS file:

src/styles/custom.css

Add example styles:

.info-box {
  border: 1px solid var(--sl-color-gray-5);
  border-radius: 0.75rem;
  padding: 1rem;
  margin: 1rem 0;
  background: var(--sl-color-gray-6);
}

.info-box h2 {
  margin-top: 0;
}

Then register the CSS in astro.config.mjs:

import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';

export default defineConfig({
  integrations: [
    starlight({
      title: 'My Starlight Site',
      customCss: ['./src/styles/custom.css'],
    }),
  ],
});

Component Example: LessonObjectives

A lesson site often needs the same opening section again and again. Create this file:

src/components/LessonObjectives.astro

Add:

---
const { title = "What You Will Learn", objectives = [] } = Astro.props;
---

{title}

    {objectives.map((objective) =>
  • {objective}
  • )}

Use it in MDX:

---
title: "Markdown Basics"
---

import LessonObjectives from "../../../components/LessonObjectives.astro";

# Markdown Basics

<LessonObjectives
  objectives={[
    "Write headings and paragraphs",
    "Create links and images",
    "Use code blocks",
    "Add frontmatter"
  ]}
/>

Component Example: CharacterStats

For a world bible or RPG-style character page, a reusable stat block is very useful.

Create:

src/components/CharacterStats.astro

Add:

---
const {
  strength = 10,
  dexterity = 10,
  constitution = 10,
  intelligence = 10,
  wisdom = 10,
  charisma = 10,
} = Astro.props;

const stats = [
  ['Strength', strength],
  ['Dexterity', dexterity],
  ['Constitution', constitution],
  ['Intelligence', intelligence],
  ['Wisdom', wisdom],
  ['Charisma', charisma],
];
---

{stats.map(([label, value]) => (
{label} {value}
))}

Use it in an MDX character page:

---
title: "Example Character"
description: "A sample character using a reusable stat component."
---

import CharacterStats from "../../../components/CharacterStats.astro";

# Example Character

<CharacterStats
  strength={12}
  dexterity={14}
  constitution={11}
  intelligence={16}
  wisdom={13}
  charisma={15}
/>

## Background

This character uses a reusable stat block.

Component Example: WorldBibleEntry

A world bible page often needs quick metadata at the top.

Create:

src/components/WorldBibleEntry.astro

Add:

---
const {
  type = "Entry",
  region = "Unknown",
  status = "Draft",
  tags = [],
} = Astro.props;
---

Use it in MDX:

---
title: "New York Harbor, 2175"
---

import WorldBibleEntry from "../../../components/WorldBibleEntry.astro";

# New York Harbor, 2175

<WorldBibleEntry
  type="Location"
  region="Atlantic Compact"
  status="Active Reference"
  tags={["harbor", "transportation", "politics"]}
/>

## Overview

This page describes the location.

Component Example: FactionCard

Faction cards are useful when summarizing groups, houses, corporations, teams, or governments.

Create:

src/components/FactionCard.astro

Add:

---
const {
  name,
  type = "Faction",
  region = "Unknown",
  summary = "No summary provided.",
} = Astro.props;
---

{name}

Type: {type}

Region: {region}

{summary}

Use it in MDX:

import FactionCard from "../../../components/FactionCard.astro";

<FactionCard
  name="House Miraculo"
  type="Noble House"
  region="Europe"
  summary="A rival house with political power, cultural prestige, and dangerous ambitions."
/>

Component Example: RelatedLinks

A reusable related-links component helps readers move between connected pages.

Create:

src/components/RelatedLinks.astro

Add:

---
const { links = [] } = Astro.props;
---

Use it in MDX:

import RelatedLinks from "../../../components/RelatedLinks.astro";

<RelatedLinks
  links={[
    { label: "Character Template", href: "/characters/character-template/" },
    { label: "Faction Template", href: "/factions/faction-template/" },
    { label: "Region Template", href: "/regions/region-template/" }
  ]}
/>

Using Frontmatter with Components

In MDX, you can use frontmatter values inside the page. This is helpful when you do not want to type the same data twice.

---
title: "Veritas Miraculo"
role: "Rival Heir Apparent"
region: "Europe"
status: "Active"
---

import WorldBibleEntry from "../../../components/WorldBibleEntry.astro";

# {frontmatter.title}

<WorldBibleEntry
  type="Character"
  region={frontmatter.region}
  status={frontmatter.status}
  tags={[frontmatter.role]}
/>

This keeps page metadata and visible page content connected.

Starlight Built-In Components

Starlight also provides built-in components that can be used in MDX and Markdoc pages. These are useful for documentation features such as cards, tabs, icons, and steps.

Example pattern:

import { Card, CardGrid } from '@astrojs/starlight/components';

<CardGrid>
  <Card title="Characters" icon="user">
    Character sheets and major figures.
  </Card>
  <Card title="Locations" icon="map">
    Important places and points of interest.
  </Card>
</CardGrid>

Use built-in components when they already solve the problem. Create custom components when your project needs its own repeated pattern.

Using Tabs in MDX

Tabs are useful for comparing code, alternate versions, or different views of the same information.

import { Tabs, TabItem } from '@astrojs/starlight/components';

<Tabs>
  <TabItem label="Markdown">
    Use Markdown for normal content pages.
  </TabItem>
  <TabItem label="MDX">
    Use MDX when the page needs components.
  </TabItem>
</Tabs>

Tabs are especially helpful for lesson pages that compare commands, code styles, or project structures.

Using Steps in MDX

Steps are useful for procedural tutorials.

import { Steps } from '@astrojs/starlight/components';

<Steps>
1. Create the component in `src/components/`.
2. Import it into an `.mdx` page.
3. Pass props into the component.
4. Run `npm run dev` and check the page.
</Steps>

This is a good pattern for installation guides, deployment tutorials, and coding walkthroughs.

Client-Side Interactive Components

Astro components render mostly as HTML by default. If you import a React, Vue, Svelte, or other UI framework component that needs browser-side interactivity, you may need a client directive.

import Counter from "../../../components/Counter.jsx";

<Counter client:load />

Common client directives include:

For most static callouts, cards, stat blocks, and reference panels, you do not need client-side JavaScript.

Component Design Guidelines

Good components are reusable, focused, and easy to understand.

Guideline Explanation
Keep one purpose A CharacterStats component should display stats, not manage the whole page.
Use clear prop names strength is better than s.
Provide defaults Defaults prevent broken-looking pages when data is missing.
Use slots for prose Slots let writers add normal content inside reusable wrappers.
Avoid overengineering Do not make a component for something you only use once.

Common Mistake: Trying to Use Components in Plain Markdown

Plain .md files are best for Markdown. If you want to import and use components, use .mdx.

Good:
src/content/docs/example.mdx

import InfoBox from "../../components/InfoBox.astro";

<InfoBox title="Tip">
This works in MDX.
</InfoBox>

Do not expect this same pattern to work in a normal .md file.

Common Mistake: Broken Import Paths

If the page fails to build and says it cannot find the component, check the import path.

Problem:
import InfoBox from "../components/InfoBox.astro";

Possible fix:
import InfoBox from "../../../components/InfoBox.astro";

The correct path depends on where the MDX file is located.

Common Mistake: Forgetting Curly Braces for JavaScript Values

Strings can use quotes. Numbers, arrays, objects, and variables usually need curly braces.

String prop:
<InfoBox title="Remember" />

Number prop:
<CharacterStats strength={12} />

Array prop:
<LessonObjectives objectives={["One", "Two", "Three"]} />

Object array prop:
<RelatedLinks links={[{ label: "Home", href: "/" }]} />

Common Mistake: Too Many Components

Components are helpful, but too many components can make content harder to edit. Use regular Markdown whenever possible.

Use Markdown for:
- headings
- paragraphs
- lists
- simple links
- simple images
- simple tables

Use components for:
- repeated visual patterns
- stat cards
- callout boxes
- tabs
- lesson objectives
- reusable metadata panels
- interactive examples

Mini Project: Build a Lesson Page with Components

Create these files:

src/components/LessonObjectives.astro
src/components/InfoBox.astro
src/content/docs/lessons/components-demo.mdx

In the MDX page:

---
title: "Components Demo Lesson"
---

import LessonObjectives from "../../../components/LessonObjectives.astro";
import InfoBox from "../../../components/InfoBox.astro";

# Components Demo Lesson

<LessonObjectives
  objectives={[
    "Understand components",
    "Import components into MDX",
    "Pass props into components",
    "Use slots for nested content"
  ]}
/>

<InfoBox title="Core Idea">
A component lets one design pattern appear consistently across many pages.
</InfoBox>

## Practice

Create your own reusable component.

Mini Project: Build a Character Sheet Page

Create:

src/components/CharacterStats.astro
src/components/WorldBibleEntry.astro
src/content/docs/characters/example-character.mdx

In the MDX page:

---
title: "Example Character"
role: "Investigator"
region: "Atlantic Compact"
status: "Draft"
---

import CharacterStats from "../../../components/CharacterStats.astro";
import WorldBibleEntry from "../../../components/WorldBibleEntry.astro";

# {frontmatter.title}

<WorldBibleEntry
  type="Character"
  region={frontmatter.region}
  status={frontmatter.status}
  tags={[frontmatter.role]}
/>

<CharacterStats
  strength={10}
  dexterity={13}
  constitution={12}
  intelligence={16}
  wisdom={14}
  charisma={11}
/>

## Identity

## Background

## Relationships

## Story Arc

Best Components for Lesson Sites

Best Components for World Bible Sites

Best Practice: Start Small

Do not start by building twenty components. Start with three:

InfoBox
LessonObjectives
RelatedLinks

Then add project-specific components only when you notice repeated patterns.

A component is worth creating when you have copied the same structure three or more times.

Troubleshooting Checklist

Problem Likely Cause Fix
Component does not render Using .md instead of .mdx Rename the page to .mdx
Build cannot find component Wrong import path Check how many ../ steps are needed
Prop displays incorrectly Missing curly braces Use {12}, {["a", "b"]}, or {variable}
Interactive component does nothing Missing client directive Add client:load, client:idle, or client:visible
Styling is inconsistent CSS not registered or class names do not match Add customCss in astro.config.mjs

Cheat Sheet

Task Pattern
Create component src/components/Name.astro
Use component in content Use an .mdx file
Import component import Name from "../../../components/Name.astro";
Pass string prop <InfoBox title="Tip" />
Pass number prop <Stat value={12} />
Pass array prop <List items={["One", "Two"]} />
Use nested content <InfoBox>Nested text</InfoBox>
Receive props const { title } = Astro.props;
Render nested content <slot />

Simple Rule of Thumb

Use Markdown for ordinary writing.
Use MDX when the page needs components.
Use Astro components for reusable UI patterns.
Use props for short structured data.
Use slots for longer nested content.
Use client directives only when browser interactivity is needed.

What to Learn Next