{name}
Type: {type}
Region: {region}
{summary}
A practical guide to building reusable Astro components and using them inside Starlight documentation pages, lessons, world bible entries, character sheets, and reference pages.
This tutorial teaches how custom components work in an Astro Starlight site, especially when writing content in Markdown and MDX.
You will learn:
.md and when to use .mdx.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.
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.
| 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.
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.
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:
Astro.props receives values passed into the component.<slot /> marks where nested content should appear.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.
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 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.
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.
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'],
}),
],
});
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"
]}
/>
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.
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.
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."
/>
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/" }
]}
/>
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 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.
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.
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.
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:
client:load for loading immediately.client:idle for loading when the browser is idle.client:visible for loading when the component enters the viewport.For most static callouts, cards, stat blocks, and reference panels, you do not need client-side JavaScript.
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. |
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.
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.
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: "/" }]} />
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
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.
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
LessonObjectives: what the learner will learn.Prerequisites: what they should know first.ExerciseBox: practice task.AnswerReveal: optional solution area.CommonMistake: warning and fix.CommandBox: terminal command with explanation.ProjectCheckpoint: confirms progress.CharacterStats: RPG-style character attributes.WorldBibleEntry: metadata panel.FactionCard: faction summary.LocationCard: point-of-interest summary.TimelineEntry: historical event card.RelationshipList: allies, rivals, enemies, patrons.RelatedLinks: connected people, places, and storylines.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.
| 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 |
| 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 /> |
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.