Koodoo
Published in

Koodoo

How do we approach styling for white-label applications?

Gif of a FE web application under different theme
One application under different themes.

TL;DR

At build time our frontend application dynamically imports a tailwind config file and a style-overrides file based on the value of a “THEME” environment variable. This allows us to apply different styles to every single component in our UI, effectively creating a customisable application.

1) Introduction 💭

2) A Component’s World🌏

  • 2.1 — Creating theme-able components 🌈
  • 2.2 — Using custom CSS classes🖌
  • 2.3 — Using default Tailwind CSS classes 🎨

3) Applying a theme to a site 🖥

  • 3.1 — Injecting a theme prop ⚙️
  • 3.2 — Applying a Tailwind configuration file 🏋️‍♀️

4) Conclusion 🔮

1 — Introduction

At Koodoo, we are building a mortgage distribution platform with the end goal to match people with mortgages that suit them, hassle free. To achieve our mission we want to make our applications available to the widest audience. A key element in our distribution strategy is to partner up with price-comparison websites such as The Times, Confused.com, and Nerdwallet and power their mortgage services.

This is a win-win: our partners can focus on what they are good at, which is gathering wide audiences onto their website and promote services via their own channels while we can focus on what we are good at — building applications to connect people with mortgages. A key element to a successful integration with a partner is styling: to be attractive we need to provide applications that integrate seamlessly with our partner’s website and brand theme.

To achieve this, we develop “white label” applications. This means that when you visit any of our partners websites, you will have access to a seamless mortgage journey designed and developed by Koodoo but each have their own unique theming and content. This approach comes with additional complexity with regards to hosting, journey design (content customization) and styling but the benefits are enormous, we can focus our development efforts on a single code base and avoid the risk of divergence that would come from maintaining multiple applications.

In this article we will focus on the styling challenges and answer the question: How do we approach styling for white label applications. Let’s dive in! 🐇

2) A Component’s World 🌍

Modern web applications often use frameworks that promote UI componentization. At Koodoo, we typically use Next.js and Tailwind CSS as our frontend frameworks which means that our UIs are made of components.

Breakdown of a User Interface into multiple components

We can define a “theme” as simply being a group of CSS rules which when put together represents the identity and the brand image of one of our partner. Theming is style customisation, so it is tightly coupled with how our components are written and styled. The first step in creating a customisable UI is creating theme-able components.

As we maintain multiple “white-label” applications across different teams we have our own React component library, that provides our applications with theme-able components.

FE application using components from our shared central component library.

Note: Having a component library is not necessary to achieve theming for white label applications, (we could also have all our components in the same code repository as our application), it just happens that as we develop multiple white label applications, having our own component library in a separate repository avoids duplications and facilitates maintainability in the long run.

2.1) Creating theme-able components 🧩 🌍

The code snippet below is for a <Button /> component and shows how our components are written to be theme-able. If we overlook the imports and the propTypes definition, our component is a simple function which does 3 things:

  1. Defines three string variables (primaryClasses, secondaryClasses and criticalClasses) holding CSS utility classes. Each of these strings define a design variant ( “primary”, “secondary” and “critical” respectively).
  2. Defines one attributes object which can contain any of the classes defined above.
  3. Returns a JSX expression: <Component > which receives each of these classes defined as a value for its className property.

Enabling design variants

This component can be rendered under different design variants depending on the “primary”, “secondary” and “critical” props it receives.

Our button component also supports “small” and “base” design variants, but I removed them from the code snippet for simplicity.

<Button/> component rendered in our Component Library explorer under the “Koodoo” theme.

⚠️ Design Variants of a component are different from the Theme: each of these <Button /> design variants should be available under a given theme:

<Button/> component rendered in our Component Library explorer under the four different themes.

2.2) Using custom CSS utility classes 🎨

To make components theme-able, we use custom css utility classes which are defined differently in different tailwind config files.

To see how this works we will focus on the theme-able “primary” button:

<Button/> component in its “primary” design variant and default state (not being hovered or activated) under four different themes.

If we zoom on the primaryClasses definition, We have styles defined for each state our <Button /> can be in (default, hover, focus-visible and pressed).

Zooming in further on the 3 utility classes used for the primary <Button/> in a “default” state, we see that these utility classes simply apply the following styles:

  1. background-colour
  2. box-shadow
  3. text-colour

If you are familiar with Tailwind, you would have noticed that these 3 utility classes are not the default tailwind utility classes, and you are right my friend ! 😉 Indeed, we are leveraging tailwind extendibility to extend it with our custom classes on top of the default classes Tailwind offers out of the box. This works particularly well for custom values such as “colour palettes” aka our own theme.

In our component library repository we define a tailwind.config.jsfile where we “tell” tailwind what each utility class (i.e. ‘bg-buttons/primary/fill/default’ etc.. ) should mean, and they each have a different meaning for each theme: one class multiple flavours 🍦.

Thanks to this file we are able to use the notation bg-[customColorClass] , text-[customColorClass]and shadow-[customBoxShadowClass]to apply each style defined in the tailwind.config.jsfile to the component. Then at build time, Tailwind generates the CSS file containing the styles defined in this tailwind.config.js file. So when our component renders, the browser will match the .bg-buttons/primary/fill/defaultclass with the CSS rule {background-color:#495ce9;} and render the following <Button />:

✨ Primary <Button /> under Theme1 ✨

For another theme, we have another tailwind.config.js where the same custom classes are pointing to different values:

My primary button under the theme3 has a different background colour and text colour:

✨ Primary <Button /> in default state under Theme3✨

Each time we want to support an additional theme, we just need to add a new tailwind.config.jsfile for that theme.

💡 We have one component, using the same className value under each theme but we simply “pair” it with a different theme file (at build time) in order to render the <Button/> under the theme we want: one class, multiple flavours! 🍻

2.3) Utilizing default Tailwind CSS classes

There are scenarios where we actually want to use the default utility classes that Tailwind offers out of the box. Imagine you have a <Button /> component (hello again 👋 😁) that you would like to render with let’s say different border-radius between two themes. My theme1 <Button /> should have slightly rounded corners and I want my theme2 <Button /> to be rounder and maybe be uppercased and bold:

✨ <Button /> Theme1
✨ <Button /> Theme2

You wouldn’t want to have to write a custom class for each border radius and font weight the buttons should have under each theme. Some styles such as border-radius, font-weight, text-transform are so common that most websites only use a small variation of values these properties can have. Tailwind already comes equipped with the default classes to support these properties:

Tailwind default border-radius styling utility classes

Instead of passing custom classes taking different meanings for each theme, we would like to simply inject different already defined classes for each theme that my <Button/> component should support.

Let’s take a look at the code again:

Notice that our <Button /> component can receive a theme property and whatever is held inside theme?.button will be applied to the Button className, effectively changing its style based on the theme props. 💡

🙋‍♂️ “ Excuse me“ …BUT, where is this prop coming from? does the application has to define a theme props and pass it into every single components it is using ? ? ? 🙀 🙀 🙀

The theme prop can still be defined in the theme files inside the component library so it can be maintained in one place an re-used in different frontend applications!

Let’s introduce some nuance: 💡 each of the themes we support is defined by not one but two files: one tailwind-config file (that we previously discussed) and one style-overrides file.

A more accurate representation of the /themes folder inside our Component Library repository.

Let’s take a look at the style-overrides file for the theme1:

💡 Notice the use of Tailwind default rounded-md class.
✨ <Button/> with border-radius value of rounded-md => theme1✨

The same file for the theme2:

💡 Notice the use of Tailwind default rounded-full as well as uppercase and font-bold classes.
✨ <Button/> with border-radius value of rounded-full (uppercase and bold text) => theme2✨

Multiple classes, multiple flavours 🌈 🍦🍦🍦

3) Applying a theme to a site:

So we have two files that fully define the style rules to achieve a specific “theme”:

  • style-overrides.js which contains default Tailwind classes
  • tailwind.config.js which contains custom utility classes

When we deploy a new instance of the front end application we specify the “theme” it should use as an environment variable and use dynamic import to require the appropriate files with a few dynamic imports:

Environment variables defined in the frontend application

💡 Using Next.js we can easily expose environment variables to the browser.

3.1) Injecting a theme prop

Based on the value of the NEXT_PUBLIC_THEME environment variable we can inject different `theme` objects inside our components at import time by wrapping each components we export from our component library with an injectTheme function.

index file in our component library folder

The injectTheme function simply relies on dynamic import to apply the desired styleOverrides :

injectTheme function definition

So every classes defined inside my theme1/style-overrides.js will be applied to my component via the “theme” prop effectively achieving conditional styling based on a “theme” required by the app.

3.2) Applying a Tailwind configuration file

We also have a “master” tailwind.config.js files in our frontend app which directly points out to the right config file defined in the component library:

When building the application, Tailwind will automatically generate the CSS files needed to apply the styles defined in the imported tailwind.config.js . The same custom class in our component will point to different styles.

Let’s visualize what has been said so far:

  • Each theme folder contains one tailwind.config.js file and one style-overried.js file with classes name and classes value specific the the theme it defines.
  • Our components are exported through one index.js file which injects a theme prop in each of our theme-able components.
Structure of the Component Library. (which could be a folder in the FE app if you don’t need an external component library)
  1. We define a component once, in one place (can be in app or in external repo if the use case is justified for your needs). This component uses custom utility classes which are defined differently in different tailwind config files.
  2. For each theme we have a tailwind.config.js which defines custom classes differently (one class which associated with different values achieve different styles)
  3. For each theme we create a style-overrides.js which contains Tailwind default classes to be applied to our components (different classes achieving different styles).
  4. We specify a theme value in our frontend application environment variables: NEXT_PUBLIC_THEME=theme1
  5. At build time, our frontend application which is configured has tailwind installed will generate the appropriates styles based (defined in tailwind.config.js ) and inject the appropriate theme prop (defined in style-overrides.js) in the components it imports.
full system view
Full system view

And voilà 🪄 🎩 🐇: 👏

If we set our environment variable “NEXT_PUBLIC_THEME=theme1” the app will have the theme1.
If we set our environment variable “NEXT_PUBLIC_THEME=theme3” the app will have the theme3.

The system is so flexible that we could have multiple application picking and choosing components and a theme from out component library:

Multiple white-label apps using themes defined in one central place.

4) Conclusion 🎬

Drawbacks ⚖️ :

  • As you can imagine our theme files are way bigger than the small extract I have shared. Their length vary depending on how complex and specific the UI design is for a given theme but they are generally MASSIVE. Which means, the creation of a new theme to integrate with a new partner still requires intensive work driven by the Design team.
  • Also the dynamic imports that the system is heavily based on and the way we integrate Tailwind with Next.JS means we have to re-build our application every time we want to switch between themes which adds an extra step in the release process.
  • Currently, the complexity of the system and the size of theme files, are the biggest drawbacks we can honestly share: complex system means a lot of overhead for a new joiners to implement a new theme and lots of time spent implementing a new design (manual and error prone). Which can impact the speed at which we can integrate with new partners.

Advantages ✨:

  • The system is incredibly flexible: we can achieve every single design requirements, style variations to meet the demands of our partners without having to develop a different application.
  • It is predictable and reusable, the themes and components created are reusable across different Frontend apps and app instances, no matter how the application evolves.
  • The main advantage of the current system, something that we often overlook in our quest for improvement: it works! We have a system that allows us to develop theme-able white label applications, yes there is room for improvements but currently this system powers every single Frontend app instances for each white-label app we develop. 👊

Future thoughts 🔮

🎬 As I write these lines, our Component Library is undergoing a massive update, we are moving away from JavaScript and Tailwind CSS in favour of TypeScript and Styled Components with Bit.dev as a library manager which will allow huge simplification of the current system, theming granularity at a component level and at runtime! Stay tune for more tech updates from Koodoo. 🤘

Let’s give back to Caesar what is Caesar’s: I am not the creator of the system presented here, merely the user and messenger. A massive thank you to all the people at Koodoo who contributed and are contributing to building great things.

I hope that sharing our experience as a development team will help you and your team succeed whatever your mission might be. 🚀 🌕

Take Care and Stay Awesome ✌️!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store