Skip to content
~/bermudev/blog
Go back

Making Theme-Aware Images Work in Astro

Updated:

Table of contents

Open Table of contents

Why this change was needed

Since a while ago, even before the theme change, I noticed a small but common issue: a black logo looked perfect in light mode, but it almost disappeared in dark mode.

So now that I have a new theme, I had to do something about it. No better time than now to fix it once and for all!

The requirements were simple:

An example of the issue can be seen below, where the logo is barely visible in dark mode (toggle the dark mode if you are using white mode to see the difference):

Logo almost invisible in dark mode

The approach

This blog theme already uses a theme toggle that updates html[data-theme] in real time. This means we can rely on CSS selectors based on data-theme and avoid adding JavaScript listeners for every image.

The solution was to introduce small opt-in attributes that apply a CSS filter only when a specific theme is active.

Option 1: Custom attributes with invert(1) hue-rotate(180deg)

In global.css we added the following rules:

html[data-theme="dark"] img[data-invert-on-dark],
html[data-theme="dark"] [data-invert-on-dark] img {
  filter: invert(1) hue-rotate(180deg);
}

html[data-theme="light"] img[data-invert-on-light],
html[data-theme="light"] [data-invert-on-light] img {
  filter: invert(1) hue-rotate(180deg);
}

This filter inverts the colors of the image only when the matching theme is active. data-invert-on-dark works well for logos that are primarily dark on transparent backgrounds and need help in dark mode. data-invert-on-light does the opposite: it is useful for light assets that need help in light mode.

The hue-rotate(180deg) part rotates the hue wheel after the inversion, which helps compensate the color shift a plain invert would introduce. In other words, it does not magically preserve the original colors, but it can produce a more natural-looking result than using only invert(1).

Then in the article itself, the behavior can be enabled only where needed:

<Image
  src={picture}
  alt="Example picture"
  width="250px"
  data-invert-on-dark
/>

And for images that need the same treatment in light mode:

<Image
  src={picture}
  alt="Example picture"
  width="250px"
  data-invert-on-light
/>

This keeps the behavior scoped to specific images instead of affecting every image on the site.

Example logo

Option 2: Tailwind class with dark:invert

If you want a more direct Tailwind option, you can also use class="dark:invert" on the image. That works great for black and white assets, but for colored images it literally inverts the colors, so it is not always the effect I want.

If you want this version instead, we do it like this:

<Image
  src={picture}
  alt="Example picture"
  width="250px"
  class="dark:invert"
/>

This is simpler as we don’t create any attribute, but as mentioned above, it performs a straight inversion, so it is better suited for black and white images than for colorful ones.

Example logo using dark:invert

Optional border control

While I was testing this, I also wanted a way to selectively hide image borders for some images, beacuse let’s be honest, sometimes it looks ugly.

For that, I added another optional attribute in global.css:

.app-prose img[data-transparent-border],
.app-prose [data-transparent-border] img {
  border-color: transparent;
}

Usage:

<Image
  src={picture}
  alt="Example picture"
  width="250px"
  data-transparent-border
/>

If you prefer, you can also solve this directly with class="border-0" or class="border-transparent". I still like the data-transparent-border approach because I can keep the intent as an HTML attribute and reuse the same tag pattern wherever I need it.

Final result with the attributes

Now with these changes we have three small reusable controls:

All three are completely opt-in and require no additional JavaScript.

And the fix is now reusable across the project and updates immediately when the theme changes, without requiring a page refresh.

Logo clearly visible after dark mode fix

This keeps logos readable in both themes while still allowing per-image control when needed — a small improvement that makes the blog feel much more polished!


Share this post on:

Previous Post
Upgrading My Terminal Setup to Oh My Posh
Next Post
Changing the Blog Theme: From Astro Cactus to AstroPaper