← Back to the blog

Images that adapt to dark mode without duplicates

Assigning semantic roles to vector layers enables a single asset to react to the user visual environment.

No duplicate versions and no CSS filters: OIM's dynamic colours recalculate the image's palette for the light or dark mode of the page that shows it, live. This is how an image adapts to dark mode without duplicating it.

Open a website in dark mode and look at its images. Everything else has adapted: the background, the text, the buttons. But the chart on the front page is still a white rectangle that dazzles in the middle of the screen. The image was made for a light background and cannot do anything else.

Today there are two options, and neither is good. One is to prepare two versions of every image, a light and a dark one, and keep both in step. The other is to invert the colours in code, which leaves logos in another colour and faces in an odd tone.

Colours with names, not numbers

In a normal design every shape has a fixed colour: this blue, that white. In OIM the designer can also give each colour a role: "background", "text", "accent". It is what any design system does on a website, carried inside the image.

There are eight universal roles: primary, on-primary, secondary, on-secondary, surface, on-surface, outline and accent. Each one maps to a CSS variable, such as --oim-color-primary. Each role travels together with the designer's original colour, which serves as the fallback.

An example: the front-page chart

Imagine the front-page chart of a news site. The designer gives the surface role to the card background, on-surface to the title and accent to the highlight of the main figure. The original colour, for example white for the background, is kept as the fallback.

The page defines its own variables. In light mode, surface is white and accent an amber. In dark mode, surface becomes a dark grey and accent a lighter tone. The same file looks right in both cases:

:root {
  --oim-color-surface: #ffffff;
  --oim-color-on-surface: #0f172a;
  --oim-color-accent: #f59e0b;
}

[data-theme="dark"] {
  --oim-color-surface: #1e293b;
  --oim-color-on-surface: #f8fafc;
  --oim-color-accent: #fbbf24;
}

Nobody has touched the file. Only the page's variables change. The designer's file, the page's stylesheet and the reader's theme are three separate things, and only the stylesheet needs updating when the brand palette changes.

The image dresses for each reader

When the image is shown, the colour of each role comes from the palettes the file itself carries. The player follows the reader's system theme. With dark mode on, it uses the palette named dark in the file; with light mode, the one named light; if the file has no such palette, it uses its default palette. A palette attribute on the element takes priority, and allow-dynamic-colors="false" turns this off.

Only colours that have a role assigned change. Photos, gradients and everything the designer left fixed stay as they were, so brand colours are not spoiled.

If an image must keep its colours even when the page has its own variables, the attribute allow-dynamic-colors="false" ignores those variables and uses each element's original colour.

Beyond dark mode

The same idea serves other things. An agency can prepare an infographic once and deliver it with each client's palette, as long as each website defines its variables. A publisher can use the same piece on its site and on a partner's site, each in its own colours, without rebuilding anything.

Web pages learned years ago to adapt to the reader. Now images can too.

Another case: documentation with two themes

A technical documentation site has an architecture diagram. In light mode the blocks are light and the arrows dark; in dark mode, the other way round. With colour roles, the diagram keeps its contrast in both themes. The designer does not have to prepare a second diagram, and the documentation team does not have to remember to update two files every time an arrow changes.

Security: a colour cannot carry code

A colour that arrives inside a file comes from a third party. If it were pasted straight into the page's style, someone could hide a CSS instruction in it that makes a request to another server. So the viewer accepts only colours from an allow-list: hexadecimal values, rgb(), hsl(), words such as transparent, and variable references. Anything else is discarded.

How to start

  1. In the OIM editor, assign a role to each colour that should change with the theme.
  2. Define the --oim-color-* variables in your page's stylesheet, with values for light and dark mode.
  3. Declare the image. Dynamic colours are active by default:
<oim-player src="front-page-chart.oim.png" lang="en"></oim-player>

The full reference of tokens and attributes is in the dynamic colours guide. If you are starting from scratch, follow the quick start.

Frequently asked questions

What happens to images without colour roles?

They are shown with their fixed colours. The viewer does not change them, even if the page defines variables.

How do I stop a website from tinting a brand image?

Set allow-dynamic-colors="false" on the element. The image ignores the page's variables and keeps the designer's colour.

Can a malicious colour inside the file load something external?

No. Colours pass through an allow-list, and values with url(), braces or quotes are discarded.

Do I need two files for light and dark mode?

No. One file serves both themes. What changes are the variables the page defines.

Further reading