A story about managing tons of colors in an Angular application

Bastien Siebman
WhozApp
Published in
7 min readOct 31, 2019
The capacity planning, a feature in Whoz that uses a lot of colors to convey information

Over the past 3 years at Whoz we went from starting a small Angular app with a team of three developers and a designer to managing a huge app with seven frontend developers and three designers. When your app has hundreds of components, with many people building it at the same time, you quickly realize not everything scales perfectly. Our translation system needed to be re-thought from the ground up. The performance of the app is being worked on every day to make sure we can serve bigger customers. Our color management system was the next obstacle on the way.

A color palette with a hundred items

At one point in the Whoz application, we had a main color, a warning color, an error color, 17 workspace colors (a user can choose its workspace color), 68 “business colors” (see below), a disabled color, secondary color and so on. That is almost 100 different colors for text, backgrounds, borders… Needless to say, without a proper system in place, it becomes a nightmare to manage.

Angular Material versus Material

We have been scratching our heads for a while with this. See, we are using Angular Material for our basic components (like tabs, buttons, and forms). Angular Material comes with a color management system. If you want a form field in error to be displayed properly, you need to give Material a color palette for errors. You need to do the same for warnings, and also for the primary and secondary colors.

So we started to dig deeper into the Angular Material colors system. And we quickly realized they messed up their implementation. Angular Material is supposed to be based on the Google Material Design Guidelines. Indeed, Google has a very lengthy and comprehensive guideline about design principles, but they are sometimes very good at not following their own guidelines.

Our solution

After brainstorming among developers and being joined by the design team, we finally agreed on something: for each “business color” (like billable, not billable, deal, opportunity…), we need at least five things:

  • a light background (a light blue to use as a background on a billable notice)
  • a text color to write on the light background (a dark grey)
  • a dark background (a dark blue to use on a tag holding billable information)
  • a text color to write on the dark background (usually just white)
  • a standalone text color (a dark blue to write a billable budget with)
  • and a set of exceptions if you want to write in business colors over business backgrounds. For example, when you need to write a billable budget inside a billable notice, the dark blue might not render well on a light blue so you might need a little variation for the text color. Or if you want to write in a dark billable blue over an opportunity light orange, you might want a different blue to have better contrast.
The capacity planning uses colors heavily. On the left, each assignment takes the color of its parent opportunity/deal (light orange or light blue). The status of the opportunity is written with a dark orange. In the middle, data about the assignments and availability uses the light blue for not billable, dark blue for billable, red for overload, grey for unavailability and orange for availability. Each color is applied to either a text, a border or a surface.

All those colors need to be accessible in a simple and clean way, making it easy and straightforward to use. So we developed a set of three SCSS functions:

  • color-on-background($name, $on-name, $variant) will give you the named color for a text when placed over the given on-color of the given variant like light or dark
  • background-color($name, $variant) will give you the named background, using the given variant like light or dark
  • color($name, $variant) is a shorthand when the on-color is the application default (in our case a white background)

Here is a couple of usage examples:

  • color: color(billable) text in billable blue on a default surface
  • color: color-on-background(billable, on-billable) text in billable blue on a light billable surface
  • color: color-on-background(billable, on-billable, dark) text in billable blue on a dark billable surface
  • color: color-on-background(default, on-billable) text in default color (grey) on a billable surface
  • background: background-color(billable) background billable (light variant)
  • background: background-color(billable, dark) background billable (dark variant)

And here is the code implementing those functions

// Get the background color for a business concept, either “light” or “dark” variant
@function background-color($name, $variant: light) {
@return map-get(map-get($whoz-colors, $name), $variant);
}
// Get the contrast color to place on a given background,
// e.g. contrast ‘available’ when placed on ‘on-default’ OR ‘default’ contrast when placed on ‘on-available’
// an optional variant is used for default text on business backgrounds, “light” or “dark”
@function color-on-background($contrast, $on-background: on-default, $variant: light) {
@if $contrast == default {
// getting the contrast color from the business palettes
@return map-get(map-get(map-get($reverse-on, $on-background), contrast), $variant);
} @else {
// getting the contrast color from the contrast map
@return map-get(map-get($business-contrast-colors, $on-background), $contrast);
}
}
// Shortcut for color-on-background with default background and light variant
@function color($contrast, $variant: light) {
@return color-on-background($contrast, on-default, $variant);
}

Each business color needs to be defined inside a dedicated map, like this one:

$whoz-billable: (
light: #e0e7f4,
dark: #0048b5,
contrast: (
light: $default-light-contrast,
dark: $default-dark-contrast,
),
);

With the following default contrasts being defined:

$default-light-contrast: rgba(31, 31, 31, 0.87); // legacy $bn-dark-primary-text
$default-dark-contrast: rgba(255, 255, 255, 1); // legacy $bn-light-primary-text

And then each map needs to be added to a mapping structure and a reverse-mapping structure for easy access by name:

// map giving easy access to the correct palette based on its name
$whoz-colors: (
available: $whoz-available,
billable: $whoz-billable,
deal: $whoz-deal,
error: $whoz-error,
not-billable: $whoz-not-billable,
not-available: $whoz-not-available,
opportunity: $whoz-opportunity,
overload: $whoz-overload,
success: $whoz-success,
whoz-website: $whoz-website,
whoz-website-emphasize: $whoz-website-emphasize,
whoz-website-highlight: $whoz-website-highlight,
whoz-website-fire: $whoz-website-fire,
surface: $whoz-surface,
);
// map giving easy access to the correct palette based on its “on-” name
$reverse-on: (
on-available: $whoz-available,
on-billable: $whoz-billable,
on-deal: $whoz-deal,
on-error: $whoz-error,
on-not-billable: $whoz-not-billable,
on-not-available: $whoz-not-available,
on-opportunity: $whoz-opportunity,
on-overload: $whoz-overload,
on-success: $whoz-success,
on-whoz-website: $whoz-website,
on-whoz-website-emphasize: $whoz-website-emphasize,
on-whoz-website-highlight: $whoz-website-highlight,
on-whoz-website-fire: $whoz-website-fire,
on-surface: $whoz-surface,
);

Here is the complete code including a couple of our colors:

$default-light-contrast: rgba(31, 31, 31, 0.87); // legacy $bn-dark-primary-text
$default-dark-contrast: rgba(255, 255, 255, 1); // legacy $bn-light-primary-text
$whoz-billable: (
light: #e0e7f4,
dark: #0048b5,
contrast: (
light: $default-light-contrast,
dark: $default-dark-contrast,
),
);
$whoz-not-billable: (
light: #f1f9ff,
dark: #87c9ff,
contrast: (
light: $default-light-contrast,
// darker text for better contrast
dark: $default-light-contrast,
),
);
$whoz-available: (
light: #fff7e8,
dark: #ffbe3e,
contrast: (
light: $default-light-contrast,
// darker text for better contrast
dark: $default-light-contrast,
),
);
$whoz-not-available: (
light: #9e9e9d,
dark: #3c3c3b,
contrast: (
light: $default-light-contrast,
dark: $default-dark-contrast,
),
);
// We also have a map to define colors of “business text” when placed on default backgrounds (white/greyish) or “business backgrounds”.
$business-contrast-colors: (
on-default: (
archived: rgba(224, 224, 224, 0.38),
available: #9c6102,
billable: #0038a5,
default: $default-light-contrast,
disabled: rgba(224, 224, 224, 0.38),
not-available: #1a1a19,
not-billable: #006bc2,
secondary: rgba(31, 31, 31, 0.6),
),
on-available: (
available: #9c6203,
),
);
// Get the background color for a business concept, either “light” or “dark” variant
@function background-color($name, $variant: light) {
@return map-get(map-get($whoz-colors, $name), $variant);
}
// Get the contrast color to place on a given background,
// e.g. contrast ‘available’ when placed on ‘on-default’ OR ‘default’ contrast when placed on ‘on-available’
// an optional variant is used for default text on business backgrounds, “light” or “dark”
@function color-on-background($contrast, $on-background: on-default, $variant: light) {
@if $contrast == default {
// getting the contrast color from the business palettes
@return map-get(map-get(map-get($reverse-on, $on-background), contrast), $variant);
} @else {
// getting the contrast color from the contrast map
@return map-get(map-get($business-contrast-colors, $on-background), $contrast);
}
}
// Shortcut for color-on-background with default background and light variant
@function color($contrast, $variant: light) {
@return color-on-background($contrast, on-default, $variant);
}
// map giving easy access to the correct palette based on its name
$whoz-colors: (
available: $whoz-available,
billable: $whoz-billable,
not-billable: $whoz-not-billable,
not-available: $whoz-not-available,
);
// map giving easy access to the correct palette based on its “on-” name
$reverse-on: (
on-available: $whoz-available,
on-billable: $whoz-billable,
on-not-billable: $whoz-not-billable,
on-not-available: $whoz-not-available,
);

An imperfect solution

We just started to work with this new system and it will surely reveal a few imperfections overtime. I was able to pinpoint three of them already:

  • we did not decide on who to follow, Material or Angular Material, and we are mixing names and concepts (like contrast and on-colors).
  • our solution does not work with themes yet, so if a client wants a different set of colors, we can’t make it happen yet.
  • the SASS functions we have designed take a little time to get accustomed to, especially the color-on-background one.

We’ll share some more feedback in the upcoming months! Stay tuned.

❤️Special thanks to Vincent, David, Léo (from the frontend team), and Camille, Sylvain, Elodie (from the design team) for helping to design this solution!

😋By the way, we are still recruiting French-speaking developers at Whoz. Feel free to check out our job offers.

--

--

Bastien Siebman
WhozApp

Asana is my secret tool. Asana Certified Pro. Author of several ebooks. Asana Community #1 contributor in the world.