Home Posts Notes About

Smooth Theme-Aware Images with CSS Transitions

August 12th, 2025 · #css #meta #tailwind

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.

Mastodon