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 a small opt-in attribute that applies a CSS filter only when the dark theme is active.

Option 1: Custom attribute 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);
}

This filter inverts the colors of the image when the dark theme is active, which works well for logos that are primarily dark on transparent backgrounds. 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
/>

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 both attributes

Now with these changes we have two small reusable controls:

Both 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