Eiko Wagenknecht
Software Developer, Freelancer & Founder

Build your own lightbox component for Astro

Eiko Wagenknecht

After setting up my new blog with Astro, I missed an automatic lightbox for images, a feature I enjoyed from WordPress. I wanted a simple, seamless solution without much custom code.

Creating a modal dialog from scratch isn’t difficult, but I prefer using a well-tested library with built-in accessibility features, which ensure the content is usable by everyone. This is why I chose Radix UI primitives, a collection of low-level headless UI components for React.

In this article, I will guide you through building a robust and accessible lightbox for Astro using React, Tailwind CSS, and the Radix UI Dialog component.

Table of Contents


Make sure you have the following installed and set up:

Install the Radix UI Dialog component as well, if you haven’t done so already:

npm install @radix-ui/react-dialog

The Lightbox Component

Let’s start building the lightbox component. There are 3 parts to it:

  1. An Astro component that will be embedded in your MDX content.
  2. The React component that uses the Radix UI Dialog component.
  3. A close icon component.

I prefer separate components for icons to allow reuse, hence the separate close icon component. And since Astro components can’t be imported into React components directly, there’s a wrapper component that uses the React component and passes the Astro icon component into it.

If you prefer, you can inline your icon in the JSX file and get away with just the jsx file.

Let’s see what the components do.


This wrapper bundles together the Astro icon component and the React modal component. The icon component is passed as a named Astro slot to the React component and can be accessed there with the closeIcon prop.

import JsxImageWithModal from "./ImageWithModal.jsx";
import X from "./icons/X.astro";

export type Props = {
  src: string;
  alt: string;

const { src, alt } = Astro.props;

<JsxImageWithModal client:visible src={src} alt={alt}>
  <X slot="closeIcon" />


Here’s the React component that uses Radix UI Dialog to create a modal dialog with an image. Radix UI is headless, so Tailwind classes are used for styling. Features include:

import * as Dialog from "@radix-ui/react-dialog";
import { Children, useState } from "react";

export default function ImageWithModal(props) {
  return (
        <Dialog.Trigger asChild>
                className="m-auto max-h-screen/50 cursor-pointer"
              {props.title && (
                <figcaption className="text-center text-sm text-text">
          <Dialog.Overlay className="fixed inset-0 bg-bg opacity-80" />
          <Dialog.Content className="fixed left-1/2 top-1/2 flex max-h-screen/90 w-screen/90 -translate-x-1/2 -translate-y-1/2 flex-col items-center rounded-md border bg-bg-offset p-4">
            <Dialog.Title className="m-0 mb-2 font-medium text-text">
              {props.title ? props.title : props.alt}
              className="h-full max-h-full"
            <Dialog.Close asChild>
                className="absolute right-4 top-4 inline-flex h-6 w-6 items-center justify-center rounded-full text-interaction hover:text-interaction-highlight focus:outline-none"

Some of the Tailwind CSS classes used in the React component are custom classes that I defined in my tailwind.config.js file:

/** @type {import('tailwindcss').Config} */
export default {
  theme: {
    extend: {
      colors: {
        text: "var(--color-text-default)",
        bg: "var(--color-background-default)",
        interaction: "var(--color-text-interaction)",
        "interaction-highlight": "var(--color-text-interaction-highlight)",
        subtle: "var(--color-text-subtle)",
        "bg-offset": "var(--color-background-offset)",
      height: {
        "screen/90": "90vh",
        "screen/50": "50vh",
      width: {
        "screen/90": "90vw",
        "screen/50": "50vw",
      maxHeight: {
        "screen/90": "90vh",
        "screen/50": "50vh",
      maxWidth: {
        "screen/90": "90vw",
        "screen/50": "50vw",

Lastly, the close icon component is a simple SVG icon from the amazing Lucide icons project that can be styled from the outside (by passing classes).


const { class: className } = Astro.props;

  viewBox="0 0 24 24"
  class:list={["lucide lucide-x", className]}
  ><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg


To use this, you need to write your content in MDX, because Astro does not support components in Markdown files. With everything set up, embedding an image with a lightbox is as simple as this:

  alt="Placeholder 150x150px"
  title="What a nice placeholder!"

And this is what it looks like when rendered (showing a placeholder image from placehold.co):

With an image that’s much larger than the screen height:

I hope you found this guide helpful. If you have any questions or suggestions, feel free to reach out to me using the links in the footer.

No Comments? No Problem.

This blog doesn't support comments, but your thoughts and questions are always welcome. Reach out through the contact details in the footer below.