Build your own lightbox component for Astro
(updated ) Eiko WagenknechtAfter 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. By the end of this tutorial, you’ll have a fully functional lightbox component that you can easily integrate into your Astro-based blog or website.
Table of Contents
Prerequisites
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:
- An Astro component that will be embedded in your MDX content.
- The React component that uses the Radix UI Dialog component.
- 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.
/src/components/imageWithModal.astro
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" />
</JsxImageWithModal>
/src/components/ImageWithModal.jsx
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:
- The preview image never exceeds half the screen height. If a title is provided, it appears below the preview image inside the modal. Otherwise, the alt text is used as a title inside the modal for screen readers.
- The modal is centered on the screen with a darkened semi-transparent background.
- The modal can be closed with the
x
button or by clicking outside it.
import * as Dialog from "@radix-ui/react-dialog";
import { Children, useState } from "react";
export default function ImageWithModal(props) {
return (
<div>
<Dialog.Root>
<Dialog.Trigger asChild>
<div>
<figure>
<img
src={props.src}
alt={props.alt}
className="m-auto max-h-screen/50 cursor-pointer"
/>
{props.title && (
<figcaption className="text-center text-sm text-text">
{props.title}
</figcaption>
)}
</figure>
</div>
</Dialog.Trigger>
<Dialog.Portal>
<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">
<div className="relative w-full">
<Dialog.Title className="mb-2 max-h-6 w-full overflow-x-auto overflow-y-hidden whitespace-nowrap font-medium text-text no-scrollbar">
<div className="inline-block pr-8">
{props.title ? props.title : props.alt}
</div>
</Dialog.Title>
</div>
<img
src={props.src}
alt={props.alt}
className="max-h-[calc(90vh-4rem)] max-w-full"
/>
<Dialog.Close asChild>
<button
type="button"
className="absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-full bg-bg-offset text-interaction hover:text-interaction-highlight focus:outline-none"
aria-label="Close"
>
{props.closeIcon}
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
);
}
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",
},
},
},
plugins: [
({ addUtilities }) => {
addUtilities({
".no-scrollbar": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
});
},
],
};
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).
/src/components/icons/X.astro
:
---
const { class: className } = Astro.props;
---
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-x", className]}
><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg
>
Update: With Astro 5 and the new SVG flag enabled, you can also use a SVG directly instead:
import X from "@/components/icons/X.svg";
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg
>
Usage
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:
<ImageWithModal
src="/assets/images/placeholder-150x150.svg"
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:
And another image with a different aspect ratio:
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.