Fabrizio Fortunato

Theming Angular at Ryanair

January 30, 2019

Theming Angular at Ryanair

Intro

Last May Ryanair announced a partnership with Laudamotion enabling the Ryanair users to purchase Laudamotion tickets directly on Ryanair website. At the same time, we are having an ongoing migration from AngularJS to Angular for some of the website pages. We already implemented theming for the AngularJS website by simply overriding the styles but for Angular applications overriding styles is not that simple.

Ryanair styleguide

Ryanair Styleguide

Before proceeding any further it is important to understand the current Ryanair styleguide used across all the teams by all the Angular applications. SASS is the preprocessor of choice of both the styleguide and the Angular applications. The styleguide is then managed in a separate repository, included as a dependency by the web apps. This is a central and single point where we can modify and update styles for all the different applications at a global level. The styleguide repository basic functionality such as SASS color variables; typography settings; useful mixins and so on. The general idea is to keep only a very small set of common styles accessible and used by all the different Angular applications.

Managing colors

When speaking about colors in CSS, and teams that operate at a different velocity, it is easy, for a designer or for a developer, to introduce a new slightly different color just by mistake. Collaboration and communication between designers and developers are key to maintain a healthy styleguide across the different products and teams. For sharing assets, for example, we are using Zeplin where the design team can name colors using the same naming convention that developers uses. Keeping not only consistency across the teams but also speaking the same language. We have then defined those named colors as SCSS variables that all the developers can reference.

To prevent the overuse of different colors we set up a stylelint rule to prevent any usage of hex colors outside of the file.

"rules": {
    "color-no-hex": true,
    "color-named": "never",
}
$primary-blue:      #073590;
$very-dark-grey:    #2e2e2e;
$light-base:        #ffffff;
$black:             #000000;
$bg-grey:           #f4f4f4;

$main-yellow:       #f1c933;
$light-blue:        #2091eb;
$standard-grey:     #828790;
$light-grey:        #d2d6d9;
$lighter-grey:      #eaeaea;
$error-red:         #cf2e1d;
$green:             #00a149;
$warning-orange:    #ffa409;
$medium-blue:       #0d49c0;

$error-red-bg:      #fde9e7;
$success-green:     #35b510;
$success-green-bg:  #e7f8e6;
$warning-orange-bg: #fff5e4;
$info-blue-bg:      #e2f4ff;

Variations, such are light shades or dark shades, are generated starting from a palette color mixed either with white or black in different percentage:

$primary-blue-light-10: mix(white, $primary-blue, 10);

Theming and encapsulation

Angular uses ViewEncapsulation to emulates the behaviour of shadow DOM by preprocessing the CSS code to effectively scope it to the components. Theming while using encapsulation makes it more difficult to override styles on the components.

We highlighted valid approaches that we could take to support theming in our Angular applications:

  • Rebuild the Angular applications per each supported theme.
  • Create a theme file which overrides styles for the different themes.
  • Using CSS variables

All the solutions have different pros and cons and will increase the complexity in different areas. What we were looking for was a solution that wouldn’t have a huge impact on our environment and deployment configuration.

A runtime solution to theming.

CSS variables looked like the best choice overall, which guarantees more flexibility, the components encapsulation and a simplified build system, given that we can change CSS variables at runtime. CSS variables have also exceptional browser support, for the unsupported browsers luckily you can use a polyfill.

Transition to CSS variables

Having a collection of different repositories, the transition to CSS Variables needed to be planned. We needed to replace first SCSS basic color variables and then the shades associated with them. We proceeded by defining a SCSS helper function that will map to a CSS variable. Using a function allows encapsulation of the variable definition and a level of abstraction around the default, setting a second argument to the variable definition used as default. This default color will be used in case no CSS has been defined, in any parent component, with that particular name. The function color, and only this function will be used from now on to define colors in all the web applications.

/**
 * Mnemonic function helper for css variables which will set also a default
 * color if the variable is not set.
 */
@function color($name, $default: null) {
  @if ($default) {
    @return var(--#{$name}, #{$default});
  } @else {
    @return var(--#{$name}, #{$name});
  }
}

By using a default we reduced the number of initial changes that a team needs to apply. We created also a node script, to reduce even more the effort to adapt, which basically goes through all the SCSS files of the repository and changes the occurrences of a SCSS variable with the function. You can find a gist of the function here.

@import "~styleguide.ryanair.com/src/brand";

._label {
  color: color("standard-grey"); //before $standard-grey
  padding-left: 0.625rem;
  position: absolute;
  transform-origin: 0 0;

  &--on {
    color: color("light-blue"); //before $light-blue
  }
}

To apply a theme at runtime we created a Service which then defines the CSS variables when needed.

import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common'; 


@Injectable({
  providedIn: 'root'
})
export class LaudamotionThemeService {
  private colors = {
    'primary-blue': '#DC1841',
    'main-yellow': '#4D4D4D'
  };

  constructor(@Inject(DOCUMENT) private document) {
  }

  apply(): void {
    for (let color in this.colors) {
      this.document.documentElement.style.setProperty(`--${color}`, this.colors[color])
    }
  }
}

Now that all the basic color are converted to CSS variables, what is missing to complete our theme are the variations or shades. The shades are generated, using a base color mixed with another. Specifically, we are using the SCSS function mix which blends two colors together by a specified percentage.

@function generate-shades-map($color-name, $color) {
  $map: (
    #{$color-name}-light-10: mix(white, $color, 10),
    #{$color-name}-light-20: mix(white, $color, 20),
    #{$color-name}-light-30: mix(white, $color, 30),
    #{$color-name}-light-40: mix(white, $color, 40),
    #{$color-name}-light-50: mix(white, $color, 50),
    #{$color-name}-light-60: mix(white, $color, 60),
    #{$color-name}-dark-10: mix(black, $color, 10),
    #{$color-name}-dark-20: mix(black, $color, 20),
    #{$color-name}-dark-30: mix(black, $color, 30),
    #{$color-name}-dark-40: mix(black, $color, 40),
    #{$color-name}-dark-50: mix(black, $color, 50),
    #{$color-name}-dark-60: mix(black, $color, 60),
  );

  @return $map;
}

@function generate-shades() {
  $map: ();
  $colors: (
    light-blue:         $light-blue,
    light-grey:         $light-grey,
    main-yellow:        $main-yellow,
    error-red:          $error-red,
    primary-blue:       $primary-blue
  );

  @each $color-name, $color in $colors {
    $map:  map-merge($map, generate-shades-map($color-name, $color));
  }

  @return $map;
}

$shades: generate-shades();

We are generating than a map of variables of all the different shades, light or dark, of a base color which is used internally in any component or application.

.fancy-text {
  color: map-get($shades, "primary-blue-light-50");
  font-weight: bold;
}

Transforming the shades is not as straight forward as per the basic colors. The shades are generated using mix function from SASS and the function doesn’t accept a CSS variable but only an actual color value. Since mix its blending colors we can only specify an actual color. We explored different possible solutions involving: using HSL to programmatic change the lightness of the color; use the new color function from CSS4 which sits currently in draft spec. Unfortunately, the latter is not supported by any browser yet and HSL won’t fit our purpose since we are blending colors not only changing the lightness.

We came up to a solution by leveraging the default color of CSS variables once again. Keeping the previous map in place but just adding our custom color function with the appropriate default which is the mix function.

/** Color Controlled light & dark **/
@function generate-shades-map($color-name, $color) {
  $map: (
    #{$color-name}-light-10: color(#{$color-name}-light-10, mix(white, $color, 10)),
    #{$color-name}-light-20: color(#{$color-name}-light-20, mix(white, $color, 20)),
    #{$color-name}-light-30: color(#{$color-name}-light-30, mix(white, $color, 30)),
    #{$color-name}-light-40: color(#{$color-name}-light-40, mix(white, $color, 40)),
    #{$color-name}-light-50: color(#{$color-name}-light-50, mix(white, $color, 50)),
    #{$color-name}-light-60: color(#{$color-name}-light-60, mix(white, $color, 60)),
    #{$color-name}-dark-10: color(#{$color-name}-dark-10, mix(black, $color, 10)),
    #{$color-name}-dark-20: color(#{$color-name}-dark-20, mix(black, $color, 20)),
    #{$color-name}-dark-30: color(#{$color-name}-dark-30, mix(black, $color, 30)),
    #{$color-name}-dark-40: color(#{$color-name}-dark-40, mix(black, $color, 40)),
    #{$color-name}-dark-50: color(#{$color-name}-dark-50, mix(black, $color, 50)),
    #{$color-name}-dark-60: color(#{$color-name}-dark-60, mix(black, $color, 60)),
  );

  @return $map;
}

Using a default we cover the base theme without the needs of an extra variable per shade percentage and color. While for all the other themes, if the shades are affected we will then define the CSS accordingly.

Conclusion

By adopting CSS variables we managed to support multiple themes in Angular applications while at the same time maintaining a simple build system and deploy strategy. All the changes are applied at runtime by simply defining CSS variables.


Fabrizio Fortunato
Head of Frontend at RyanairLabs @izifortune