Branding Huddle’s UI using CSS Variables and webpack

Mohamed Eltuhamy
huddle-engineering
Published in
9 min readAug 8, 2018
Photo by Steve Johnson from Pexels

🎁 Branding is a feature

Huddle is a document collaboration tool that facilitates securely working with external companies and contractors in a secure and audited manner. As such, we provide enterprise companies the ability to have a custom branded look and feel of Huddle to make it feel like it’s truly theirs.

Huddle’s default branding
A company’s custom branding

This article briefly describes how we went about implementing a branding solution that is scalable (i.e. we can support many custom brandings for many customers) and won’t cause regressions as we develop UI code.

👷A little bit of context…

Branding is powered via a custom domain. For example, if the user is on “acme.huddle.net”, we should show the “acme” branding.

Huddle’s UI is loaded by a single JavaScript file that loads the required chunks at run time. We currently do not have server-side rendering as we are transitioning from a legacy server side framework to a single page application. This means that we can’t use the server to load custom CSS or JavaScript as it is an entirely different code base and architecture. We have to resort to using the client to do everything at run time in the browser.

The UI supports all the evergreen browsers and IE 11. This is based on statistics of our real user base that we review regularly. IE 11 is used by nearly 33% of our users. The rest is a combination of Chrome, Firefox and Edge.

In the past, the branding feature was implemented as ugly CSS overrides that load based on the domain. This was frequently a cause for regressions as we refactored UI code markup and class names without thinking about the CSS overrides. We decided it was time to implement a real solution…

💅 Using CSS Variables (custom properties) for branding

The basic idea is that we have a set of branded colours and images. We can use those colours anywhere in our CSS code base.

A branding colour theme
How branding colours are used in the UI

Now, all we have to do is figure out which branding file (like the one above) to load given the domain name. This is where webpack comes in!

📦 Using webpack to load the right branding

Let’s store all our branding files into a folder called ‘brandings’. We’ll also give each file a file extension of .branding , so we don’t confuse them with regular CSS files.

In our webpack entry file, we can do a simple check like this:

For example, if the location is “https://acme.huddle.net”, the above function will dynamically import “./brandings/acme/acme.branding”. We just need to configure webpack to load .branding files as inline CSS, using css-loader and style-loader:

And that’s it! This solution, although very simple, has a lot of advantages:

  • It’s scalable: If we want to implement a thousand brandings, we can do so easily without impacting performance. That’s because our import is a lazy one — webpack will only load in the one branding instead of all the brandings.
  • It’s simple: we didn’t have to write a lot of custom code — the code is very easy to understand. Webpack automagically did the work for us. And using CSS variables means that we can implement new branding files every time we get a new customer who wants branding.
  • It’s cached when not changed: letting webpack handle the chunking means it can spit out hashed chunk names. When the branding file changes, the file name and hash will change, and the cache will bust. If the branding file doesn’t change, the file name and hash won’t change, and therefore it will remain in the users cache. Awesome!

But then you’re probably wondering, what’s the catch?!

Hope this diagram makes it clear where the problem is

😭 IE 11 Support

CSS custom properties aren’t supported in IE 11. At Huddle, a large percentage of our customers use IE 11 so it’s not an option to fall back to ‘no branding if you’re in IE 11’. We had to find a solution.

The obvious answer is to use PostCSS to rewrite our custom property usages to just pure CSS (there’s a plugin for that). But a PostCSS solution would require us to have both the CSS variable definitions and the CSS of our whole app at build time.

A naive workaround for this problem would be to re-build our app’s CSS several times, once for each branding. Then use a run-time check to load the CSS for our branding. The problems with this approach are:

  • It’s not scalable: if we have several branding files, we’d multiply the build time for our CSS
  • It’s hard to do it in webpack: we would need to use extract text plugin to get the output of the css as built by webpack, then use PostCSS to process it several times. Dynamically loading the correct CSS file at run time will require some kind of runtime code that would be custom written and hard to generalize. The magic of just using import isn’t there anymore.

To save half our users from a non-branded experience, we decided to write our own webpack plugin that solves this problem in a more scalable, simple way.

🎨 Branding plugin

Remember the ugly overrides we used to do (see ‘a bit of context’ section above)? It turns out that ugly overrides actually work. They’re just ugly to write and maintain. What if we automatically created those overrides and only load them in IE 11? That’s the basic idea behind branding plugin. The cool bit is that it will just work with import statements for branding files. Oh, and those overrides won’t get loaded if the browser supports CSS variables.

Let’s first see how to use it!

The loadBranding function is unchanged. I’ve copied it here so you don’t have to scroll up:

Now, we just need to tell webpack how to handle branding files. This is where the plugin comes in.

Notice that we tell webpack to handle .branding files using brandingPlugin.createLoader() and we also tell webpack to handle .css files using brandingPlugin.createCSSLoader — this is because branding plugin needs access to our app’s CSS. Any CSS file can use CSS variables we define in our branding. We could also handle SCSS or LESS in the same way.

And that’s it!

You get all the benefits of the original CSS variables solution, except when CSS variables aren’t supported, a file will be included that includes overrides for all the rules that use CSS variables.

We’re planning to open-source branding plugin so everyone can use it soon. Keep an eye on this blog and @HuddleEng on twitter for updates!

Update: branding-webpack-plugin is now open source! You can install it via npm.

How it works

An overview of things produced by the loader and plugin combined

From the diagram above, we can see that we need two ingredients to produce the assets we need for the solution.

  • Branding file: this will be imported from anywhere in JavaScript at run time. This is what defines the values for our CSS variables that are used elsewhere in the app’s CSS. The branding webpack loader will take this in, pass it through CSS loader (to handle dependencies like image URLs) and will compile to a runtime check and compiled branding as labelled above. IE 11 overrides are produced later by the plugin.
  • App CSS: this is also imported anywhere. It is passed through Branding plugin’s CSS loader. This loader holds on to it for later processing by the plugin.

Once webpack has finished processing its dependencies and ran everything through its loaders, the branding plugin finally kicks in and produces the IE 11 overrides. It does this in the following way:

  1. Use PostCSS to parse the app’s CSS
  2. Use PostCSS to extract the CSS variable keys and values
  3. Go through the app CSS and find usages of CSS variables. When it finds a usage, it extracts the rule. At the end, the output will be only the rules that use variables. These are the overrides
  4. Now that we have the CSS rules, we substitute the CSS variable usages with the actual values we got from step 2 above

🚧 We still have some gotchas!

The solution we end up with is great, but unfortunately, the fallback for IE 11 has some issues.

The first is CSS specificity wars. The problem is best illustrated with an example. Imagine a CSS file in our app which uses a branding colour:

.element {
color: var(--branding-color);
}

This will cause a IE 11 overrides file that looks like this (assuming the branding colour is “red”)

.element {
color: red;
}

But what if later on in our app’s css, we override the color with a non-branded colour? The problem is, the IE 11 override produced by branding plugin will win because it is placed at the end of the page. This might be un-anticipated behaviour. Unfortunately there is no real solution for this problem, we have to work around it by avoiding overrides like this in our app’s CSS.

The next problem is to do with how Microsoft Edge handles relative image path names. Edge supports CSS variables (🎉) but it has this problem. Let’s go through another example. Imagine we have branding and app CSS as follows:

/* App CSS: */
.logo {
background: var(--logo-img);
}
/* Branding file */
:root {
--logo-img: url(./img.png);
}

Because we pass the branding file itself through CSS loader, webpack will nicely handle the dependency on ./img.png. The output for the compiled branding will look something like this:

:root {
--logo-img: url(0f19286421752a0f9b76d1973e6f590a.png);
}

Now, because we don’t have a public path, Edge will load the image using a relative path. But relative to what? What we’d like is that it’s relative to the location of the JavaScript file — and this is what happens on all other browsers. But Edge, unfortunately, loads the image relative to the page it lives on. The reason is that we inline the branding CSS into the page using a <style> tag, and it looks like what the browser does in that case is not really well defined.

So we have to use another workaround for images. We dealt with it by using JavaScript to set the image. We did this by making an import to the branding file give you the CSS variables in JS:

import brandingVars from './acme.branding';
console.log(brandingVars);

The output from the above will be an object with the branding variables:

{
"--logo-img": "url(0f19286421752a0f9b76d1973e6f590a.png)"
}

We can then use the value to set the background image using JavaScript.

🤗 The result

Our branding solution worked well for us. We migrated all the ugly overrides (there were about 40+ files doing slightly different things!) to the new branding approach and we have a well defined process for introducing new ones. We’re no longer scared to refactor markup and CSS in case it breaks branding.

So what’s the future?

We’re already thinking about how we can use CSS variables to allow customers to implement their own branded experience using a self-service tool in the product, and how we might be able to generate a colour theme just using a logo. The possibilities are endless, and it starts with a well-defined way of specifying colours and images. Hooray! 🎉

--

--