Figures and captions in MDX
Published · Tagged mdx, astro, markdownMarkdown gives you  for images and nothing for captions. In a plain .md post you could write the <figure> by hand in raw HTML, but Astro doesn’t optimize a local image inside raw HTML, so you’d lose resizing and modern formats.
MDX fixes that. An .mdx post is Markdown that can also import and use components, and this blog has one for exactly this job.
The component
Figure takes an imported image, its alt text and an optional caption. It outputs nothing but a figure, an img and a figcaption:
---
import type { ImageMetadata } from "astro";
import { Image } from "astro:assets";
interface Props {
src: ImageMetadata;
alt: string;
caption?: string;
}
const { src, alt, caption } = Astro.props;
---
<figure>
<Image src={src} alt={alt} />
{caption && <figcaption>{caption}</figcaption>}
</figure>
There are no classes and no styles. LunarCSS already styles a figure with a border, and sets its figcaption centred in a striped rule.
Using it
At the top of the post, below the frontmatter, import the component and the image:
import Figure from "../../../components/Figure.astro";
import moonrise from "../../../assets/moonrise.svg";
Then use it anywhere in the post:
<Figure src={moonrise} alt="…" caption="Moonrise, drawn as a small SVG." />
Here’s the result:
Astro added the image’s width and height at build time, so the page doesn’t jump when it loads. This one is an SVG, so it’s served as-is: it’s sharp at any size already. A PNG or JPEG would also be resized and converted to a modern format.
Why the caption is a prop
It would be neater to write the caption between the tags, like HTML. But when MDX finds text on its own line inside a component, it wraps it in a paragraph, and a p inside a figcaption is both odd HTML and odd-looking: LunarCSS draws a bracket beside every paragraph. A prop keeps the caption plain text.
When to use it
Use Figure whenever an image needs a caption. For an image without one, the Markdown syntax is shorter and gets the same optimization, in .md and .mdx posts alike.
Only write a post in MDX when it needs a component. MDX parses a post as JSX, so a stray < or { in the text can break the build, where plain Markdown would have printed it.