Ionic CSS Variables Dynamic Theme Generator

One of the major changes to theming in Ionic 4 is the use of CSS variables, which are simply CSS values that can be reused across multiple elements or components. Unlike variables in Sass or LESS that need to be compiled, they can be changed and interpreted by the browser on the fly. This is an extremely powerful feature for web components that use the Shadow DOM, like Ionic 4.

Demo of the Ionic 4 dynamic theme builder app

The following lesson will teach you the basics of CSS Variables, then we show you how to create a dynamic theme generator that works across all platforms. A common use-case for variables is to build a frontend UI that allows users to switch between light and dark mode. A great real-world example is the Dark Mode for YouTube.

CSS Variable Basics

CSS variables are really simple. The most basic usage is to define a set of global variables for common theme elements like color, border radius, shadows, etc. It is common to set global vars on the :root because it ensures that they will be picked up by all other elements in the DOM.

In the example below, we define a variable named --primary-color as the color orange. Then we use it as the background on a button var(--primary-color).

:root {
  --primary-color: orange;
}

button {
  background: var(--primary-color);
}

It’s also smart to provide a fallback value, just in case the variable is undefined.

button {
  background: var(--primary-color, green);
}

Variables cascade just like regular CSS. If you override a variable, its children will inherit the new value.


:root {
  --primary-color: orange;
}

article {
  background: var(--primary-color);
}

button {
  --primary-color: blue;
  background: var(--primary-color);
}

In the snippet above, the article will be orange, while the button will be blue.

How Ionic uses CSS Variables

Ionic has a large set of global CSS variables that can be used to style virtually all aspects of the application. You can find them in the src/theme/variables.scss file. I recommend checking out Ionic’s new Color Theme Generator and use Coolors.co if you need inspiration for a color palette.

The Ionic 4 color generator tool for CSS variables

The Ionic 4 color generator tool for CSS variables

Building a Custom Theme Generator with Ionic 4

Our goal is to build a theme controller that makes composing unique themes as easy as possible. When we finish the service, creating a new theme is as easy as passing it a new object with 1 to 8 colors.

ionic generate service theme

We will be installing a utility library called color to calculate the correct tint, shade, and contrast for our base colors.

npm i color

Pure Function for Generating a CSS String

Our goal is to send a few pass in an object of input colors, then return a CSS string that overrides the global Ionic CSS variables. The code below is quite simple - pass in an object of colors, then use them to define our CSS as a string.

You can include this code inside the `theme.service` or any other file of your choosing. I omitted most of the CSS to keep it short, but you can find the full source code on github.

import * as Color from 'color';

const defaults = {
  primary: '#3880ff',
  secondary: '#0cd1e8',
  tertiary: '#7044ff',
  success: '#10dc60',
  warning: '#ffce00',
  danger: '#f04141',
  dark: '#222428',
  medium: '#989aa2',
  light: '#f4f5f8'
};

function contrast(color, ratio = 0.8) {
  color = Color(color);
  return color.isDark() ? color.lighten(ratio) : color.darken(ratio);
}

function CSSTextGenerator(colors) {
  colors = { ...defaults, ...colors };

  const {
    primary,
    secondary,
    tertiary,
    success,
    warning,
    danger,
    dark,
    medium,
    light
  } = colors;

  const shadeRatio = 0.1;
  const tintRatio = 0.1;

  return `
    --ion-color-base: ${light};
    --ion-color-contrast: ${dark};

    --ion-color-primary: ${primary};
    --ion-color-primary-rgb: 56,128,255;
    --ion-color-primary-contrast: ${contrast(primary)};
    --ion-color-primary-contrast-rgb: 255,255,255;
    --ion-color-primary-shade:  ${Color(primary).darken(shadeRatio)};

    // omitted other styles, see full source code
`;
}

Theme Generator Service

Now that we have the logic for creating a CSS theme, we just need some basics JavaScript to update it on the DOM. The service below provides a setTheme method to update ALL global variables, and another setVariable method to create/update an individual CSS variable.

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

@Injectable({
  providedIn: 'root'
})
export class ThemeService {
  constructor(
    @Inject(DOCUMENT) private document: Document
  ) {}

  // Override all global variables with a new theme
  setTheme(theme) {
    const cssText = CSSTextGenerator(theme);
    this.setGlobalCSS(cssText);
  }

  // Define a single CSS variable
  setVariable(name, value) {
    this.document.documentElement.style.setProperty(name, value);
  }

  private setGlobalCSS(css: string) {
    this.document.documentElement.style.cssText = css;
  }

}

Saving Themes Across Sessions with Ionic Storage

If the user refreshes the page their current theme will revert back to the default. That’s not a great user experience, but we can fix it easily with the Ionic Storage package.


import { Storage } from '@ionic/storage';

@Injectable({
  providedIn: 'root'
})
export class ThemeService {
  constructor(
    @Inject(DOCUMENT) private document: Document,
    private storage: Storage
  ) {
    storage.get('theme').then(cssText => {  // <--- GET SAVED THEME
      this.setGlobalCSS(cssText);
    });
  }

  // Override all global variables with a new theme
  setTheme(theme) {
    const cssText = CSSTextGenerator(theme);
    this.setGlobalCSS(cssText);
    this.storage.set('theme', cssText); // <--- SAVE THEME HERE
  }

}

Using the Theme Service in a Component

All the hard work is done. Now we just need to inject our ThemeService into a page or component and put it to use.

Change the Global Theme

In my home page, I have defined a set of themes that can be updated when the user clicks a button.

import { Component } from '@angular/core';
import { ThemeService } from '../theme.service';

const themes = {
  autumn: {
    primary: '#F78154',
    secondary: '#4D9078',
    tertiary: '#B4436C',
    light: '#FDE8DF',
    medium: '#FCD0A2',
    dark: '#B89876'
  },
  night: {
    primary: '#8CBA80',
    secondary: '#FCFF6C',
    tertiary: '#FE5F55',
    medium: '#BCC2C7',
    dark: '#F7F7FF',
    light: '#495867'
  },
  neon: {
    primary: '#39BFBD',
    secondary: '#4CE0B3',
    tertiary: '#FF5E79',
    light: '#F4EDF2',
    medium: '#B682A5',
    dark: '#34162A'
  }
};

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  constructor(private theme: ThemeService) {}

  changeTheme(name) {
    this.theme.setTheme(themes[name]);
  }
}

Now we can just bind the theme options to a few buttons:

<ion-button (click)="changeTheme('autumn')">Autumn</ion-button>
<ion-button (click)="changeTheme('night')">Dark</ion-button>
<ion-button (click)="changeTheme('neon')">Neon</ion-button>

Controlling CSS Animations with Variables

Another cool trick we can pull off with CSS variables is to control animation parameters. Let’s setup an animation for a rotating spinner:

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

.spin {
    animation: spin var(--speed) linear infinite;
}

Then we can define another handler in our component.

@Component(...)
export class HomePage {

  changeSpeed(val) {
    this.theme.setVariable('--speed', `${val}ms`);
  }
}

And lastly, we setup a buttons so the user can change the animation rotation speed in the UI:

<ion-button (click)="changeSpeed(2000)">Slow</ion-button>
<ion-button (click)="changeSpeed(500)">Fast</ion-button>

The End

Hopefully this article gave you a solid understanding of CSS variables and how awesome they are as a theme building tool. The theme generator code in this lesson is not perfect, but it could easily be tweaked to fit the needs of your app. Let me know if you have any questions by leaving a comment below.

Questions?

Ask questions via GitHub below OR chat on Slack #questions