Applying Visual Themes with SCSS
Tags: tutorial, themes, webdev, scss
Published: 2023-09-17 15:02:56

When you have a large legacy web application, implementing a dark mode theme is a pretty daunting concept. Especially when your CSS is all over the place and you have a number of third-party components to worry about.

The concept of implementing dark mode in our eMarketing product was a natural continuation of conversations the team had had around usage of dark mode in HTML emails and email clients.

In the past, the team hadn't given too much consideration to the organisation of CSS. This was in part due to the lack of any overarching design system. As a result, I had appointed myself as our de facto UX expert and the task of figuring out the best way of implementing CSS themes fell to me.

## Clean Styling

There's no point in even attempting to implement colour schemes in a web product unless the CSS is clean. Modern frameworks rightly separate out HTML, JavaScript and CSS into separate files but our 15 year old application (which was based on an even older ASP solution) hadn't followed these common processes.

This multi-step process was probably the most time consuming element of the implementation of visual themes, but well worth it even as an unrelated task.

We reviewed all of our existing CSS files, split component specific styles into their own separate files. There is no right approach here, what matters most is consistency. I split our CSS between external and internal components; so I created sub-folders for each component provider (i.e. BootStrap, Telerik, etc) and sub-folders for application areas (i.e. application settings, user security, etc) before migrating our CSS to files within these folders. Anything remaining that couldn't easily fit into a category was migrated to catch-all CSS.

Once this process had been completed, we installed & configured WebCompiler to automatically compile our styles from the SCSS.

Finally, we reviewed the usage of inline CSS and CSS style blocks within our CSHTML content (there was a lot of this) and migrated anything theme specific to the new SCSS files.

There would be more clean-up work on this later, but this put is in a good place for future development and allowed us to split our generated CSS between common application elements which are required all the time and screen specific styles which just needed to be loaded alongside certain screens.

Because of the amount of CSS being moved around, it's wise to test changes and compare screens against unmodified versions. This allows us to catch any precedence issues caused by moving things about.

## Create SCSS Variables and Review Colour Scheme

While the team had taken care to ensure that the existing colour scheme was consistent throughout the product, inevitably there were areas where variations had been inadvertently introduced within our CSS.

This was now the perfect time to review all colour codes and fonts within our SCSS files and question any inconsistencies. This gave us the opportunity to not only streamline the product appearance but to keep the number of CSS variables as low as possible.

Even with this, after our first pass, we still created over 100 CSS variables. The impact of this clean-up on product appearance was instantly obvious.

## Theme Generation

The next step was to create our first theme from the SCSS variables. I declared each theme in its own file, this isn't _strictly_ necessary but it makes maintenance significantly easier.

Each file consists of a list of variables and a theme object which uses these variables, looking something like the below example.

```
// branding colours
$branding-primary: #105CC6;
$action-edit-button: #00a2e8;

// fonts
$font-primary:"Helvetica Neue", Helvetica, Arial, sans-serif;
$font-legend: Arial, Helvetica, sans-serif;
$font-header: Calibri, Arial, sans-serif;
$font-code: Lucida Console;

$standard-theme: ( 
    // branding
    branding-primary: $branding-primary,
    branding-primary-dark: darken($branding-primary, 15%),
    branding-primary-edit: $action-edit-button,
    branding-primary-edit-dark: darken($action-edit-button, 20%),

    // base fonts
    font-primary: $font-primary,
    font-legend: $font-legend,
    font-title-area: $font-header,
    font-code: $font-code
);
```

To generate the final CSS, we need to take our themes and use an SCSS mixin to define additional CSS rules that trigger their usage.

We achieved this by specifying a CSS class name within the `<html>` DOM element which would dictate the name of the current theme. The standard theme is enabled by the `theme-standard` class and the dark theme is enabled by the `theme-dark` class etc.

```
// import themes from external files
@import 'standard.scss';
@import 'dark.scss';

// add themes to an array
$themes: (
    standard: $standard-theme,
    dark: $dark-theme,
);

@mixin theme() {
    $array: $themes;

    @each $theme, $map in $array {
        html.theme-#{$theme} & {
            $array-map: () !global;

            @each $key, $submap in $map {
                $value: map-get(map-get($array, $theme), '#{$key}');
                $array-map: map-merge($array-map, ($key: $value)) !global;
            }

            @content;
            $array-map: null !global;
        }
    }
}

@function themeValue($key) {
    @return map-get($array-map, $key);
}
```

We can then modify our SCSS to include a reference to the `themeValue` function, this will trigger generation of multiple CSS lines, one for each possible theme.

```
body {
    @include theme() {
        font-family: themeValue(font-primary);
    }
    font-variant: none;
}
```

## Theme Selection

Finally, the only thing left to was to configure our theme. This needs to happen as high up the page load as possible in order to prevent any flashes of un-styled content.

We could of course, just apply the style class to the `<html>` DOM object and be done with it; however, most applications provide a "System Theme" option and it won't take too much work to implement this ourselves.

In this example, we already support `theme-standard` and `theme-dark`, we'll now introduce `theme-system` as a third option and apply the light or dark theme automatically.

```
// are we using the system theme
const isSystemTheme = theme == "theme-system";
if (isSystemTheme)
{
    // detect colour scheme
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        theme = "theme-dark";
    }
    else
    {
        theme = "theme-standard";
    }
}

setVisualTheme(theme, isSystemTheme);
```

We'll then clear the existing theme and apply the new one. We'll want to note if we're using the default system theme, this will allow us to change the theme without refreshing the page.

```
function setVisualTheme(themeName, isSystemTheme) {
    // remove any existing theme
    const $html = $("html");
    $html.attr("class", function(i, c){
        return c.replace(/(^|\s)theme-\S+/g, '');
    });

    $("html").prop("usingSystemTheme", isSystemTheme);

    // set new theme
    $("html").addClass(themeName);
}
```

We can intercept changes to the system theme by using the JavaScript below. If the operating system theme is changed, then this will be automatically passed to the browser window and we can ensure that this update is reflected on the page:

```
window.matchMedia("(prefers-color-scheme: dark)")
    .addEventListener("change", event => {
        if ($("html").prop("usingSystemTheme")) {
            let targetTheme = "theme-standard";

            if (event.matches) {
                targetTheme = "theme-dark";
            }

             setVisualTheme(targetTheme, true);
         }
    });
```

As a final step, we might also want to update the colour of the browser header to match the theme colour. This provides a more cohesive feel in mobile environments.

We can do this by adding an additional meta value within our document:

`<meta name="theme-color" content="#4285f4" />`

We can't extract the theme colour directly from CSS, but we can apply the appropriate class to a hidden DOM object and then read that colour using JavaScript and apply it to the theme-color object. This approach also resolved an issue with a third party component which required the colour scheme to be set using JavaScript.

##

In conclusion, implementing visual themes in a product that hadn't ever been written with this in mind was tricky work; however the majority of work was in cleaning up the existing CSS implementation.

There were a number of points along the journey where the benefit of what we were doing was obvious and this was before we'd even implemented our first colour scheme.

If this journey proves anything, it's how important it is to segregate our UI components and associated CSS. This is something modern frameworks do very well, but can be lacking in legacy applications.