9 Decisions were made when developing a unified Dark Mode
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.
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
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.
Decision 1: Choosing a Color Contrast Benchmark
Early on, we had to decide which benchmark to use for color contrast checking.
- The Web Content Accessibility Guidelines (WCAG) 2 is the de facto standard for color contrast checks.
- WCAG 2 distinguishes between two text sizes and two font weights.
- Advanced Perception of Color Algorithm (APCA) is a new algorithm that can differentiate 15 font sizes and nine font weights.
- APCA is evolving and is part of the WCAG 3 draft, available since 2021.
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.
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.
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.
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.
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.
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.
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.
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?
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.
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.
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
Benefit 2: Intuitivity
Benefit 3: Flexibility
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.
- 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:
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
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.