Lunar Blog

Figures and captions in MDX

Published · Tagged mdx, astro, markdown

Markdown gives you ![alt](src) 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:

A grey moon over a dark, rocky horizon
Moonrise, drawn as a small SVG.

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.