9 Decisions were made when developing a unified Dark Mode

Simon Schmidt
idealo Tech Blog
Published in
11 min readMar 23, 2023

--

The focus of this pamphlet is on the idealo Shopping App, which serves as the mobile counterpart to Germany’s leading price comparison platform.

The Starting Point

When iOS 13 was released in September 2019 and introduced system wide Dark Mode, we managed to add the same to our iOS app shortly after. For each color value of Light Mode, we chose a corresponding color from our existing corporate design and did not bother to name it. This led to a kind of inverted color system that lacked flexibility. The design process for Light Mode was straightforward, yet the challenge of creating an appealing Dark Mode equivalent persisted.

About a year later, we started the development of the Dark Theme for Android. We followed the Material 2 Guidelines closely to improve in comparison to iOS colors. It involved semantic color naming, color inheritance, and new color values defined by Material Palettes. Although the result looked fresh, we found it difficult to use. Over time, colors were misused in other contexts, e.g., the @color/order-timeline was used as a border in a view unrelated to orders, among others. The names were not very self-explanatory. We missed the opportunity to create a working system that everyone could understand.

Screenshots of both idealo Shopping apps Dark Mode, left iOS and right Android
Desaturated iOS vs. tinted Android idealo Shopping App

The Goal

Our goal this year was to address the color disconnect between our two mobile platforms and improve accessibility (A11Y). However, we discovered that our main corporate colors worked well for text in only three out of ten cases:

  • Yellow on Dark Background
  • Red on Light Background
  • Blue on Light Background
The five main corporate colors of idealo with their luminance contrast value, only three of ten are passing
For green and orange replacements exist already (small circles). Unfortunately, they do not provide enough contrast in APCA too.

To achieve our goal, we set out to develop a unified Dark Mode for both mobile OS, streamline speccing, introduce a better naming scheme for more intuitive usage, and additionally achieve A11Y compliance.

idealo color palette showing the status quo situation
Existing corporate colors didn’t give many options for dark shades

Decision 1: Choosing a Color Contrast Benchmark

Early on, we had to decide which benchmark to use for color contrast checking.

Earlier, I expressed my preference for APCA over WCAG 2, but I’d like to offer another perspective. WCAG 2 was released in 2008, the same year the iPhone revolutionized the industry. Yet, we still rely on this same tool even though many innovations have emerged since then, such as Retina Displays, Dark Mode, and more recently, variable fonts. Over a decade and a half have passed since WCAG 2 was introduced, and it’s time for an update that reflects the current state of technology.

In different samples, we found APCA to be less forgiving, meaning a switch to APCA would only risk exceeding WCAG compliance. Considering all these factors, we chose APCA over WCAG 2 as our benchmark.

Graph that shows reflectance on X scale and CIE Lightness on Y scale. WCAG is a linear line while APCA is a curve bending into CIE territory, indicating the eye perception is an irregular function
The perceived contrast of dark colors deviates the most from the linear WCAG 2 measurement. That is my interpretation of all that math nerd talk involved in the development of APCA at least.

Decision 2: Establishing Premises

After some thought, we decided to integrate new color hues for Dark Mode into the “Light Only” corporate design palette. The premises are:

  • Stay as close to Corporate Design as possible
  • Stay as close to the current Light Theme implementation as possible
  • Achieve APCA compliance in both Light and Dark Theme

Decision 3: Deciding on a Base Design System

Integrating iOS “Dynamic System Colors” into a corporate design is not straightforward, and the Apple Human Interface Guidelines (HIG) offer limited guidance in this regard. On the other hand, Material 3 Custom Colors offer very detailed instructions, but the outcome was disappointing and barely usable. It quickly became clear that we could not use either OS solution as a solid foundation.

Therefore, we conducted an inventory check to see what implicit system we had already created and explored ways to translate it into comprehensive documentation with minimal changes.

Decision 4: Finding Surface Colors

Defining background and surface colors first was a given, as they provide the context for any contrast considerations. Our app de facto uses 4 Surface Colors for different purposes:

  • Background
  • Tile Surface
  • Shade on Tile Surface 1
  • Shade on Tile Surface 2

When choosing a background color for Dark Mode we had to consider:

  • The existing desaturated iOS background color
  • The ‘by the book’ Android background color (#121212 with a Primary Color Tint)
  • Our idealo product images, which all have white backgrounds and are not isolated

On any view with product images our color histogram in Dark Mode spikes. You typically want to avoid maximized contrasts (same with text colors, btw) — meaning any pitch black AMOLED mode was out of the question, we were anchored in bright tints. AMOLED displays appear especially dark, the pixels themselves are emittive. No background lighting that bleeds through on black screens is necessary.

Screenshot of the Product Details Page in Dark Mode on the right side, showing a product picture with white background. On the right a mocked lightness histogram of that view spiking in light area.
Our product images cause high contrast in Dark Mode

As a starting point, we reverse-engineered the eye-pleasing Twitter Dark Mode. Then we began to alter composition values and saturation, always checking for the complete picture across the main user journey. Existing Android colors served as placeholders for the content colors.

A multitude of circles with gradients tested  for background surfaces
Tested shades of Gray

Note that color contrast checks are not helpful for deciding on surfaces as the desired shade differences typically barely move the needle.

In the end, we settled on a slightly tinted dark gray as the background, a significantly brighter default tile color, and one subtle and one less subtle shade for differentiation on a tile.

Background colors and description how they are constructed (blue/white with opacity values on black)

Decision 5: Converting Corporate Colors to Dark Mode

We experimented with reverse engineering Apple’s system color conversion for Dark Mode as well as studying Material 2 and 3 palettes. However, we wanted to create a solution that wasn’t biased towards any OS and recreate the Dark Mode colors as close to our Light Theme as possible. Our approach was simple:

We increased the HSL lightness of each color until Zebra gave the green light for the desired text sizes and weights (mostly 12–16 pt Medium). Notice how the decision about surfaces we made beforehand is important for the contrast to measure the text against.

Animation of figma process, original blue is getting brighter by adjusting L value of HSL until figma plugin Zebra is signalling compliance
Increasing lightness until the color becomes compliant

This approach leads to some quite bright colors due to the strict APCA requirements, especially with regard to blue action text. Even when defining our use cases as only spot-readable. To counter a large deviation from the corporate color we decided to decouple the color of action text from the color from the brand color that could be used on buttons.

Contrast measurements that played a role here were a button to the background, text to the button, and text to the background.

different shades of blue with their corresponding APCA measurements
Trying to find a fitting Dark Mode equivalent for brand blue proved to be a challenge
orange and blue as button color and independently as text color with their corresponding APCA measurements
End Result for both Primary and Secondary Brand Color

Decision 6: Improving Accent Color Usage

For some specific states, we had colored surfaces that didn’t look particularly good in Dark Mode, e.g., in the context of price alerts. Here we wanted to improve by adding a subtle outline that elevates the background color into a less muddy impression. Coloring the text itself helped emphasize the color compared to the previous, more neutral approach.

To define the new colors, we used our corporate hues and adjusted the color until we met APCA conditions for text. The surface and outline colors use the same color with reduced opacity. The contrast that was important to us in this case was text to surface and outline to the background.

Note: Measuring contrast with opacity requires a solid sample of the area.

legacy implementation of accent colors (left) vs. new approach (right) that emphasizes colors in comparison
Legacy and new design of Accent Colors
resulting new color palette after introducing new Dark Mode colors
Newly introduced hues for Dark Mode marked with the yellow triangle

Decision 7: Pattern Translations

This might have been one of the more controversial decisions made. When studying changes from Material 2 to Material 3 something peaked our interest. Before, only in Dark Mode surfaces layering on top of each other got shaded differently to highlight elevation whereas Light Mode relied on shadow size. Material 3 is more consequential and now also shades surfaces with elevation, which contradicts the physical lighting doctrine. Now, could we adopt this change too?

Two surface on top of each others, on the left Material 2 (both surfaces have a shadow) , on the right Material 3 (no shadows but surface on top has darker color)
Difference in elevation handling between Material 2 and Material 3

Sadly not. Removing outlines and shading all buttons in Light Mode instead did not turn out satisfying. Outlines seemed to provide better contrast and helped maintain a clean and minimalistic appearance. On the other hand, introducing an all-outline style in Dark Mode was not working either. Here a fill created a more appealing visual impact against the dark background, making the button stand out better too. We decided for an irregular transformation of the outline in Light Mode vs. filled in Dark Mode.

Three figures showing combinations of outline buttons in both Light and Dark Mode, filled buttons in both and a mix of outline in Light and filled in Dark.
A) outline only vs. B) shade only vs. C) a wild mix

Now, why are dark colors behaving so differently? My hypothesis: spatial frequency. If you increase luminance, as you do when using Light Mode instead of Dark Mode, cutoff frequency increases as well, making thin lines easier to distinguish. On the other hand, with lower luminance, you need broader strokes/fills to get the same contrast impression. Learning about spatial frequency is a rabbit hole and I would be greatful if anyone with more knowledge could challenge me.

Graph showing effects of luminance on perceived contrast (Y scale) and spatial frequency (X scale). Higher luminance means farther cutoff and higher contrast threshold.
Low Vision Enhancement with Head-mounted Video Display Systems: Are We There Yet? — Scientific Figure on ResearchGate. Available from: https://www.researchgate.net/figure/Normal-log-contrast-sensitivity-for-different-average-luminance-levels-see-legend_fig3_327286481 [accessed 21 Mar, 2023]

Decision 8: Semantic Naming Scheme

We already had system-induced inheritance supporting semantic naming in Android. Yet in iOS, we used hard color hue names from the corporate design. Gray50 would be the ambivalent name for a light gray as well as the corresponding dark gray in Dark Mode.

Now we wanted to introduce a new, more approachable naming scheme to both platforms as well.

Benefit 1: Adaptability

three buttons, above each a textcolor change needs to be referenced 100 times in code while on bottom only one change is needed with a semantic color name
Exchanging values is easier if an abstraction layer exists that can be referenced.

Benefit 2: Intuitivity

on the left a UI snippet, on the right a column of color hues and a column of more intuitive semantic color names. the latter can be easily matched on the UI snippet.
Reducing the guesswork by using color names that communicate intent.

Benefit 3: Flexibility

two surface stacks (background, surface, surface shade), one light and one dark mode. background color has the same value as the surface shade in light mode but differs in dark mode as it would create a visual ‘hole’ way more apparent than in light mode.
Colors can have a common name in Light Mode and differ in Dark Mode. This allows for context-sensitive conversion.

Old but true: Naming is hard

Deciding on semantic color names proved to be as hard as they say. A few examples:

  • Do we want to mimic Material 3, in which relationships between text and surface are highlighted? E.g., primary and onPrimary or error and onError. Even when this seems super helpful at first glance, it turned out difficult to adapt. For one it increases color amount significantly. Additionally, our existing system does not feature many 1:1 relationships. We decided to create our own syntax to fit our system.
  • How many identical blue text colors do we want to allow? It can be used in both informative and action contexts, both on surfaces and buttons. Finding a common purpose is not possible, although the color will be the same. The decision was to only use textInfo despite it not fitting 100% in every context.
  • How are signal colors called? We use red in contexts where it does not mean critical or error, e.g. negative price trends. Same for green, it's not always success. In the end, we settled on ~negative and ~positive as the lowest common denominator.
  • How are icon colors handled? In most cases, they need the same distinction as text, but calling them text~ would be confusing. We also found some instances where an independent icon color would work better. Icons are valued differently than text in APCA too. We chose to introduce a specific icon color for each use case, e.g. iconPositive.
two offer cells with a delivery truck indicating shipping status in green and yellow with a corresponding text. on the left icon and text have the same color hues while on the right side they are not identical but visually more pleasing due to the difference in stroke widths of text and icon.
Difference between identical color for icon and text (left) vs. visual balanced (right)
  • How to call surfaces? Android used a system based on elevations in Material 2 which we translated into numerics, e.g. surface_16. However In Material 3 primary, secondary, etc. are used to differentiate between the types. We opted for the second option because a) iOS system colors use the same logic and b) we managed to reduce the surface count drastically down to 3. At the same time, primary communicates the purpose better than surface_0.

After some discussion, the following naming scheme emerged:

exemplary abstract UI, lines lead to semantic color names of each UI element.
Exemplary naming scheme

Our solution differs from Material 3’s approach of creating one-to-one connections between surfaces and text. Instead, we prioritize flexibility in our design. Exceptions being, e.g., textOnBrand for text on buttons or badges and inputOn… for inputs on a specific surface. We use a syntax that includes ‘type,’ ‘intent,’ and optional ‘intensity’ to describe styles consistently across our design system. By starting with the ‘type,’ similar styles in Figma are conveniently located next to each other.

Resulting Syntax:

{ type } { intent } { intensity (opt.) }

Here are all the syntax options:

  • types: surface, text, border, divider, icon, input
  • intents: onSurface, onBackground, onBrand, background, brand, positive, warning, negative, info, neutral, highlight, primaryToTertiary, primaryToSecondary
  • intensities: primary, secondary, tertiary, strong, subtle
Complete Picture of the resulting Color System

Decision 9: Design Token Figma Integration

There are several ways to use Figma as a single source of truth for color values (and other Design Tokens). In the last idealo Hackweek we built a POC and tested the technical requirements. We felt the technical change on top of the complete renaming and redefinition of colors was a bit too much to swallow, however. The reliance on unofficial plugins like Design Tokens or Token Studio is a risk, but first and foremost building a sophisticated process in a messy design system is taking a second step before the first one and raising implementation effort.

While a seamless integration into design tools is still a desirable scenario, we will postpone any efforts until the new color system has proven itself.

Outlook

Having outlined the 9 key decisions that culminated in our new color system, all that remains is to put it into practice. That includes implementation and, of course, testing. While we don’t expect a significant impact on our metrics you can never be sure and we are ready to iron out any issues that may arise. The same goes for using the system — we are excited to receive feedback from devs and designers alike. I might post a follow-up on how we did in the future.

Edit 2024: The follow up blog post can be found here: https://medium.com/idealo-tech-blog/unified-dark-mode-reality-check-1d2cb136ebeb

Looking for a job where you can work fully focused and enter your personal flow state of mind? Check out our vacancies.

--

--