When Ghostty came out, I wrote this blog post which contained a few screenshots. I forgot how I managed to have both light and dark mode work seamlessly in Eleventy, so I figured I'd write it down for future me or anyone that sees this.
The Pattern
I use an Eleventy shortcode that conditionally renders different images based on theme preference. The basic version wraps images in figure tags with captions:
config.addShortcode('image', (src: string, alt: string, withDarkMode = false) => {
const lastDotIndex = src.lastIndexOf('.');
const basePath = src.slice(0, lastDotIndex);
const extension = src.slice(lastDotIndex);
const lightPath = `${basePath}-light${extension}`;
const darkPath = `${basePath}-dark${extension}`;
return `<figure>${
withDarkMode
? `<img class="not-dark:hidden" src="${darkPath}" alt="${alt}">` +
`<img class="dark:hidden" src="${lightPath}" alt="${alt}">`
: `<img src="${src}" alt="${alt}">`
}<figcaption>${alt}</figcaption></figure>`;
});
Smooth Transitions
The images would swap instantly when toggling themes, which felt jarring. Adding Tailwind's transition utilities creates a cross-fade effect:
withDarkMode
? `<img class="not-dark:hidden not-dark:opacity-0 dark:opacity-100 starting:dark:opacity-0 transition-discrete transition-opacity duration-1000" src="${darkPath}" alt="${alt}">` +
`<img class="dark:hidden dark:opacity-0 not-dark:opacity-100 starting:not-dark:opacity-0 transition-discrete transition-opacity duration-1000" src="${lightPath}" alt="${alt}">`
: `<img src="${src}" alt="${alt}">`
The starting:
modifiers prevent the fade animation on initial page load, which I borrowed from how Tailwind's own docs handle theme switching.
Usage
Now {% image "ghostty-screenshot.png" "Terminal with ligatures" true %}
automatically looks for ghostty-screenshot-light.png
and ghostty-screenshot-dark.png
, then smoothly transitions between them when you toggle themes.
Trade-offs
Benefits:
- Smooth 1-second cross-fade between theme variants
- Both images preload—no flicker on theme switch
- Simple to maintain with clear naming conventions
Drawbacks:
- Downloads both variants even if user never switches themes
- Requires maintaining two versions of every screenshot
Notes
This approach works well for static sites where you control every image. For dynamic content or high-traffic sites, you'd want lazy loading for the hidden variant or CSS filters for automatic adjustments.
References
Figured how to create custom shortcode after reading this article by Anh.