Angular: Multiple Themes Without Killing Bundle Size (With Material or Not)

Keep users happy with a choice of themes

Jul 24, 2019 · 5 min read
Picture by Chris Martin from Pixabay

I assume you’re comfortable with the concept of Angular, Angular Material, and how to theme with Angular Material. There are many guides available, or use the official documentation.

This guide is also interesting if you don’t use Angular Material, it can easily be applied to other UI libraries or your own library.

Angular Material

Angular Material makes our life simple when we need to theme our app.

Working with a preprocessor style, such as Sass or SCSS, you simply use mix-ins provided by the UI library and you’re done.

An example to theme

You can add as many themes as you want and it can be interesting for your users to choose between them easily, with one click, to provide a site well-suited to your user.

But… You have some pain points to resolve:

  • You have to distinct each theme by providing a class in your main CSS file.
  • You have to create a logic to manage these classes inside your app when a user selects a theme.
  • Every theme will include the mix-in and you’re increasing the bundle size. It’s a lot to download for your users.
  • You will have a lot of unused CSS properties in your file.

Let’s say we have an app containing two themes: the default and the dark theme.

We can create a _theme-base.scss to be DRY:

And our two themes:

Default theme
Dark theme

And in the end, we create the classes to apply the themes:

Classes for our themes

With this configuration, our bundle size is 184KB.

(I used the option extractCss in angular.json to get the CSS file instead of the JS file, to be consistent between prod and dev. You will understand why later in this article)

I recently worked on a project where there were multiple themes (more than six). The bundle size was heavy: 1.56MB.

After optimization, I succeeded in reducing this size to ~150KB per theme, thus 90% of the CSS was unused by the user and it has to download it!

Managing Multiple Themes

Here is a guide I’ve created to manage multiple themes without killing the user experience with a heavy style-bundle size.

1. Create a separate folder to store your theme files

As you’ll have all your themes stored in one place, it will be easy to locate them, like this:

2. Modify angular.json to extract CSS themes into files

When you develop, Angular converts your styles into JS files. It is faster during development.

Just for the tests, you can disable this behavior by adding an option in your angular.json. This is activated by default in your production’s options.

Now, we have to create a separate CSS file per theme. To do this, you have to modify the angular.json once again by specifying the files you want to create.

input: takes the path of where your file is located.
lazy: if true, indicates to Angular that we don’t want to add the file to the head of our HTML automatically.
bundleName: is just the output name.

At this point, if you modify the property lazy: true for one of your themes, you will directly see the result in the browser. But, the real interest is to load it dynamically.

We can do this, for this example, with a service.

3. Create a service to manage themes

This service is necessary to do two things:

  • Add the theme’s CSS file in the HTML’s head.
  • Switch between themes and dark mode.

I will focus on the first task, and just show how I did it with the second one.

Take a look at this implementation and read the explanations below:

Service to handle themes

When you’re using a service, you won’t be able to get the renderer.

If you are not comfortable with the notion of Renderer2 and DOM manipulation, I recommend you read my previous article.

  • Line 26 — Sometimes, Angular had this kind of problem, that’s why they created RendererFactory2. With this, we can create a renderer and use it inside a service. You also inject the document, to get access to the <head> tag.

Don’t worry about the parameters set to null. The first parameter is hostElement with type any and the second is type with type RendererType2|null . So, there is no problem to set them to null.

  • Line 45 — The tricky part is the loadCss method. Using our renderer, we create the appropriate link tag which will be added inside the head as a child element (line 52).
  • Line 51 — To avoid having a page without style, we wait for the end of onload by giving it the resolve of our promise. So, the promise ends when the CSS file is loaded.

The rest of the code assures that we only use one theme at a time, to improve the critical path rendering of our page.

4. Apply the service to your component and let the magic happen

It’s finally time to use our ThemeService and see how incredible it can be!

Here is the HTML to test our theme:

And its component:

And you can see the result directly in your browser.

But the real interest is in your bundle size. Instead of the 184KB in the beginning, you should have:

  • theme-default.css = 85.5KB
  • theme-default-dark.css = 72.5KB

Suggestions to Optimize

This is an example of the implementation but it is not the best way.

In a real-world app, you have to manage the cache so the user doesn’t download the file again and again (imagine if your user changes their mind every day?). This can be achieved by using a service worker.

Also, the theme initialization can be implemented directly in the APP_INITIALIZER. This can be interesting and avoids Angular Material shouting for:

Normal, because we delayed the load of the Angular Material theme.

Have a nice day and happy coding!

Better Programming

Advice for programmers.


Written by

Web developer & Angular Specialist

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade