Designing a Dynamic Color System for Hulu Ad Manager

Nate Smith
15 min readAug 28, 2021


Hulu Ad Manager is one of Hulu’s enterprise tools. It’s a website where advertisers can set up, manage, and analyze the performance of ad campaigns on Hulu. In this article, I’ll discuss improvements we brought to its design system in early 2021. In close collaboration with our engineering team, we developed an all-new approach to UI colors that launched in Hulu Ad Manager and that has served as a foundation for color in our broader enterprise design system. We called this approach a dynamic color system.

The dynamic color system is predicated on three main breakthroughs in understanding:

  1. Grasping Digital Colors with HSL
  2. Differentiating Foreground & Background
  3. Arranging UI in Light & Space

Each of these ideas represented a progression that became core to the overall system, and below I’ll reflect on their influence on the design of Hulu Ad Manager. While I’ve included detailed descriptions of each of the breakthroughs, the article is hopefully interesting regardless of whether it’s skimmed or read thoroughly. That is, the system is not “all-or-nothing” — every design system is different, and individual insights discussed here may be helpful when applied in a completely different way. We’d love to know how you think this can be taken even further!

Hulu Ad Manager, circa 2020 — prior to design system work

As we started auditing Ad Manager’s design system, we found several global problems with its colors. Firstly, there were excessive ranges of both shades and tones of gray (in the following discussion, I’ll use the term shade to refer to a color’s lightness value and tone to refer to its temperature or hue). There were over 50 different shades of gray which were fortunately linked to global variables. While many of the gray values had cool tones — aligned with Hulu’s global brand — some of the gray values had less saturated tones, which looked warm by comparison. Although this excessiveness in shades and tones was merely a result of different designers and developers having worked on it at different times, it constituted a bloated system that was unsustainable to maintain.

50+ Shades of gray variables in Ad Manager’s codebase

Secondly, certain color combinations were rendering text illegible, signaling broader issues with contrast in the design system. Although we considered WCAG standards a minimum requirement, contrast goes far beyond text legibility. Contrast is about how our eyes move across a visual composition; things with greater contrast draw more attention, implicitly signaling greater importance. Color is just one of many methods to create contrast. With typography, weight and scale, for example, are arguably more effective methods to create contrast than color. Although color will be the focus of this article, our overall approach to improvements in the design system incorporated a holistic consideration of contrast.

Finally, the various gray variables used names that didn’t follow a clear and consistent convention. Some names referred to hierarchy (e.g. color-primary or color-text-secondary) while others referred to use-case (e.g. color-text-placeholder). Furthermore, virtually none of the coded variables matched color styles in our Figma libraries; this meant that, for every single element in a new design, a developer would need to make an assumption about which variable should be used.

Design System Inheritance

There was clearly an opportunity to simplify and harmonize Ad Manager’s array of grays, but where do we start? Given its position in a broader set of enterprise tools, ideally Ad Manager could inherit colors from the higher-level enterprise design system, which had a grayscale with 7 steps. When comparing the Ad Manager grayscale to the enterprise grayscale, however, the enterprise scale clearly lacked sufficient variation in shades; there weren’t enough steps, and the jumps between steps were too big. If we were to have mapped each Ad Manager gray to its closest enterprise counterpart, Ad Manager would have lost the majority of its variation, potentially breaking parts of the design and hampering accessibility. Hulu had other high-level libraries — such as for Data Visualization, Hulu Web light-mode, and Hulu Web dark-mode — each of which had a similar lack in variation.

Some of Ad Manager’s grays mapped to grays from related design systems

While sharing some of these early investigations with the enterprise design team, it became clear that other designers sought more variation in the enterprise grayscale in general. It turned out, in order to have more flexibility, some designers had already been using the Hulu Web dark-mode grayscale in conjunction with the enterprise grayscale. This revealed the opportunity to design a new color system that could flex beyond just Hulu Ad Manager. To get there, though, we needed a better way to think and talk about individual color values, their differences, and how they fit together in a scale.

First Breakthrough: Grasping Digital Colors with HSL

HEX color values, as commonly used and as shown above, seem to indicate very little about how one color compares to another. What might the difference be between #F7F7F9 and #DDE0E6? There is a pattern, it’s just very abstract. Using a different color format, though, we can more intuitively understand differences between color values. HSL—which stands for hue, saturation, and luminance—is a different way to tell computers what color to use. It’s a color format for humans. Furthermore, it’s a color format that has ubiquitous browser support, yet that has been widely under-utilized in web development.

We realized that, if we were to define a common hue and saturation, we could create consistency across gray tones. A hue of 220deg and a saturation of 14% closely matched the intended cool-gray tone, so these numbers became foundational to the overall approach. The luminance value is a percentage from 0 to 100 which describes the shade of a color. A luminance of 0% is pure black, while a luminance of 100% is pure white. Luminance was the perfect way to describe the steps between shades along our grayscale.

HSL values for Ad Manager’s new grayscale

While hue and saturation created the harmony we sought in the color tone, now the question was how we could simplify the scale by picking the right quantity of color shades. One approach could have been to, for each of Ad Manager’s existing gray variables, just convert their HEX value to an HSL value. With converted HSL values, we could have simply replaced each of the hue and saturation numbers with our new, common numbers. While this may have improved surface-level visual consistency across the tool, it would not have addressed the excessive quantity of shades or the disconnect between designers and developers when it comes to naming and usage.

So we instead wanted to find the sweet spot, where we would have sufficient variation to adequately map the old values to the new values, and yet where we would have a small enough number of values so that each would have an intentional purpose. With this, we started to look at various increments along the scale. We considered using regular steps of five or ten.

Regular steps of 5 (top) and 10 (bottom), mapped along linear paths

Both of these mapped along linear paths, consistent jumps between each step from 0 to 100. As we were testing across use-cases, though, we noticed that subtle variations on the far ends of the scale were particularly important (especially in supporting both light and dark mode), while subtle variations in the middle were largely unnecessary. Increments of five on the ends and ten in the middle left us with a set of intermittent steps that looked more like it mapped along an easing path.

Intermittent steps, mapped along an easing path

15 Shades of Gray

At this point we started to feel pretty good about how this selection of 15 luminance values could be used for our new grayscale. It seemed to satisfy most use-cases we were testing, and when mapping the old Ad Manager grays to these new ones, most shades were accounted for.

Naming Conventions

In the past, each gray would be assigned a number for its sequence in the scale. For example, in a grayscale with 15 shades, values would be named something like Gray-1 through Gray-15. With this convention, what if we wanted to add a step in between? Say, we needed an extra step between Gray-6 and Gray-7. Do we call it Gray-6.5? Do we shift them up—Gray-7 is actually now Gray-8, and so on? These solutions seem far from ideal.

Thinking in terms of luminance values, however, greatly improved our naming conventions. With the new system based on HSL, we could simply name each gray with its luminance value. So Gray-20 would refer to a dark-gray that’s 20% lighter than pure black (and 80% darker than pure white), rather than the “twentieth gray” in a fixed scale. Now, our naming convention leveraged meaningful, intuitive numbers on which designers and developers could align.

Old gray variables mapped to new grayscale values

With this, it was straightforward to implement the new grayscale in our Figma library. We created color styles for each of the grays, one-by-one. These styles could be used in any design file, remain linked to the library, and then updated later if necessary.

Simplified grayscale values, implemented as Figma color styles

Color Styles vs Variables

We did encounter one interesting constraint of the color styles feature in Figma: individual numeric values could not be linked to higher-level variables. That is, when creating the color styles, the hue, saturation, and luminance numbers needed to be re-typed every time. This obviously introduced a lot of potential margin for error.

At the same time as we were optimizing the new system for our design tool, we were also working with our engineering team to optimize the new system for code. We found that CSS custom properties would let us define variables for hue and saturation, and then we could re-use those higher-level variables across all 15 shades. This realization about coded (sub) variables would end up having a major impact on the dynamism of the final system, but at this point, it just seemed like a notable convenience of code versus a design tool.

New grayscale as CSS Custom Properties (Variables)

Second Breakthrough: Differentiating Foreground & Background

At this point we had a set of gray values that could be used, but now the question was how should they be used? To get at this, we started exploring visual compositions with the new grayscale, combining backgrounds and text in various ways. In the examples below, compositions are shown from left to right, with backgrounds that progressively decrease in contrast with text. That is, the light backgrounds get darker, and the dark backgrounds get lighter, while the text colors remain the same. This exercise helped us determine primary gray values for text that would offer maximal flexibility across backgrounds, while still meeting AA contrast standards.

Light backgrounds progressively get darker, and dark backgrounds progressively get lighter, while the text colors remain the same

Visualizing Accessible Combinations

We created matrices to better understand how all the gray values could be used together for text and backgrounds. In the figure below, the x-axis displays the range of background colors and the y-axis displays the range of text colors. The text-background combinations outside the lines meet AA contrast standards, while the ones in the center do not.

Contrast matrix showing our 15 shades of gray, with the specific combinations that meet AA standards

Beyond text, though, it was important that icons should also meet contrast standards. With this, we started to realize that we could segment all the various elements of the UI into either foreground or background. The foreground is for communication. Foreground includes elements like text and controls. The background is for structure. Background includes elements like containers and dividers. When we started to think of interface elements in terms of foreground and background, we thought less about absolute color values, and more about contrast — how color values can be used together to create an intentional hierarchy.

Foreground text shown on various background shades

Designing Systems Alongside Features

This deep-dive into the design system was happening in parallel with feature designs. This was actually helpful, as we could test the new grayscale in a practical context. At the time, we were working on net-new flows for account management. This would require a range of interconnected pages, and as such, we were considering a multi-panel navigation model where hierarchical panels would be displayed from left-to-right. A sub-navigation item would be selected in the left panel, a specific item in the middle panel, and the details of that item in the right panel. This was a great opportunity to stress-test our new 15 grays for text, icons, containers, and dividers.

Multi-panel navigation model designed with account management features

The example above shows two cursors, each triggering an item’s hover state. This pattern where an “uncontained” item could have various states — each indicated by a particular shade of gray — was interesting to consider in the context of this multi-panel navigation model. Uncontained items like these could be displayed on a range of different backgrounds, and yet people would need a reliable way to know what exactly a particular shade is indicating. We were first using the following shades:

  • Shade of left panel background: Gray-95
  • Shade of left item container on-hover: Gray-90
  • Shade of right panel background: Gray-100
  • Shade of right item container on-hover: Gray-95

Notice that, across both the left and right panels, the same contrast was achieved between the panel’s background and the item’s container: that is, both hover states used a shade that was 5% darker than the background. However, this almost seemed like too much contrast, with a preferable hover-state shade being closer to 3% darker, requiring values outside of the 15 grays we had been considering up to this point. Something like a Gray-97 seemed perfect for hover states on Gray-100, and Gray-92 on Gray-95. And yet, if we were to keep adding shades like these, we could have easily perpetuated the excessiveness and unintentional usage we sought to address. It seemed like there was another opportunity here. The key was to build from the coded variables and to consider panel backgrounds not just as arbitrary colors, but as separate and distinct elevation levels.

Third Breakthrough: Arranging UI in Light & Space

This brings us to the third and final breakthrough: the color system’s dynamism doesn’t just stem from understanding colors with HSL and differentiating foreground & background, but also by arranging UI in light & space. When we look at any visual composition, we interpret it based on things we know from the real world about how light behaves in space: light is usually cast from above; things that are closer to the light source are usually brighter than things that are farther away.

A popular optical illusion shows a checkerboard, with a cylinder sitting on top of it. The cylinder casts a shadow onto the checkerboard. We see squares A and B, which are clearly opposite; A is a dark square, and B is a light square. Right?

Checkerboard optical illusion, where squares A and B appear to be opposites

And yet, if we look closer at the “actual” color that’s displayed on a screen or printed on a page, we notice that squares A and B are in fact the exact same color. Despite that we know there isn’t a real shadow, and there isn’t a real physical object, we cannot help but interpret the image as if there is. This illusion nicely illustrates our tendency to interpret two-dimensional, digital things based on what we know about physical things. Even if we don’t make stylistically skeuomorphic design decisions, the underlying benefit of skeuomorphism remains: metaphors from the real world help us understand things about the digital world.

So we took the 15 shades of gray that we had been testing and laid them out in relationship to a light source. Could it be, that dark-mode surfaces are simply farther away from the light source than light-mode backgrounds?

15 shades of gray shown in relationship to a light source, dark surfaces are further away than light ones

To use the previously discussed example of the multi-panel interface, we can separate the different surfaces and see hierarchical relationships in 3D. Some surfaces are above others, and the surfaces interact with each other in predictable ways.

Spatial relationships of multi-panel navigation model shown in 3D

Let’s consider something demonstrated in the optical illusion: the differences between the gray values shape our perception of the space. For the uncontained items discussed, rather than hard-coding values that intuitively seem right, we can instead perform a calculation to achieve consistent contrast between luminance values, mimicking a similar relationship to a light source. For both the Gray-95 left panel and the Gray-100 right panel, we can take those “base background lightnesses” and subtract 5% from each, giving us luminance values with consistent contrast.

Subtracting 5% from the luminance of the background achieves a consistent contrast across backgrounds

Although the previously described set of 15 variables was a great starting point, this realization was the key to making the system truly dynamic. In code, numbers and math are easy, meaning a function could be used to dynamically calculate a color that achieves consistent contrast in any context. Let’s dive into the mechanics of how this can be achieved in code.

Below, a new set of “background offset” (bg-offset) variables are shown, where now, each variable is driven by the base lightness of the background (base-lit-bg). In light mode, that base lightness started at 100% (white), while in dark mode, 0% (black). The luminance value of any given gray could be determined by the distance offset from its background (“offset” in that, in light mode, the luminance value would be subtracted—as in the example above—while in dark mode, the luminance value would be added).

“Background offset” variables: added (in dark mode) or subtracted (in light mode) from the base lightness of the background

Given the behavior of custom properties in CSS, the base-lit-bg variable could be re-defined for particular styles. This meant that we could create classes for surface elevation levels, where a re-defined base-lit-bg variable would cause all bg-offset variables to respond dynamically to the shade of the underlying surface.

For both light and dark mode, surface elevation levels are classes that re-define the base lightness of the background

With this, we had all the pieces to assemble the final dynamic color system. A flowchart illustrates the way the system breaks down from the high-level appearance (light or dark mode) to the individual use-cases.

Flow of variables and calculations: appearances → elevation levels → offsets → base values → use-cases

To recap,

There were three main breakthroughs that led to the development of this dynamic color system:

  1. Grasping Digital Color with HSL: Using HSL color values made it easy to create a simplified, harmonious grayscale.
  2. Differentiating Foreground & Background: Segmenting elements into foreground and background helped us more intentionally create contrast.
  3. Arranging UI in Light & Space: Considering how UI elements are arranged in light and space led to a code-based, dynamic system that’s rooted in principles of visual perception.

Color has served as the foundation for a range of visual updates that have fundamentally improved the aesthetics and usability of Hulu Ad Manager.

Hulu Ad Manager, before and after
Hulu Ad Manager, circa early 2021

Future potential

There are numerous opportunities to scale this design approach. We can consider implications for saturated colors beyond gray. Our primary utility colors, for example, can also be reconfigured according to their contrast with their container. Here you can see in light and dark-mode, colors from low to high contrast. This may be a useful approach when working on data visualization, for example.

We can also consider flexing to different use-cases for different brands. Here you can see an example, where we can simply change two values, and fundamentally change the look and feel of all the primary colors in the UI.

This was a really fun project to work on, and I can’t thank my team enough for the support in pursuing such an endeavor. David, Eric, Lily, Allie, Linda, Claudette, Vlad, and Kasper, this wouldn’t have been possible without you.

Thanks for reading!