In recent years, we have become accustomed to using light and dark themes everywhere we can: in the IDE, in the browser, on the desktop, often even on mobile devices. Usually, when we are told about different themes, we imagine a different set of colors, within which the main color is either close to white or dark gray.
With the development of the Internet, more and more applications are moving to mobile devices and the browser. Today we will consider the last method of creating applications, i.e. in the form of websites, taking into account both modern applications and enterprise applications that should be compatible with older versions of browsers.
As part of this article, we will try to take a closer look at various options for how we can allow users to choose from various themes, as well as customize them for their needs. Since all applications have their specifics from the technical stack to business requirements, we will try to build on the basic approaches without sticking to specific libraries or frameworks. When describing all the approaches, I will give some examples based on real solutions that were used in the development and applied in finished projects.
If we try to divide possible solutions into some groups, we can distinguish the following options:
Before describing specific solutions, it is important to think about how the user will customize the theme. Depending on how much customizability we need, we can try to prepare a set of custom themes in advance, or we can provide the user with the ability to autonomously customize themes. There is also a combined option, in which the user will be able to further customize a predefined theme, modifying those styles that do not suit him.
When we are working with a set of predefined themes, we can store all our styles in pure CSS, thus having a set of files that we can connect to our website and give the browser access to the styles that are bound to the base class. This approach is used by many style libraries, in particular Material.
As an extension to this approach, we can add an intermediate conversion of styles to CSS, based on predefined data in any format and user data that can be provided through the customization system. This will be, in fact, a styling application. With this approach, I can suggest storing data in JSON format since JSON objects make it easy to work with JS – all modern systems support it, and its structure is quite simple and allows you to compress it before you transmit it to the end client.
With that in mind, if you have a set of styles or a mechanism to generate that set, we can discuss some options for how to apply the generated CSS and JSON style data to the client.
When working with already built style files, the main question will be: how exactly will we send data to the user? We can focus on a single API for serving styles, which will serve the desired file guided by the user’s choice, or we can give public access to a folder with all available themes to enable our application to download the desired file guided by the name of the theme.
The first option is pretty good since we don’t even have to save the original CSS files. The backend API will accept the theme name as a parameter, take the template and data from the database and use them to generate the CSS that will be returned to the user. With this approach, we can control the state of the returned file by quickly changing information, as well as easily (using a logging layer) tracking how often users work with certain themes. Also, this option saves us from the browser caching problem when we need to generate a new hash for the client browser to download the new version of the styles. Apparently, since we are getting rid of the browser cache, we should add the server cache.
The second option is more suitable for cases where the generated styles rarely change. In this case, we give the website access to the styles folder and upload the required file through the theme name by substituting this name when generating the index.html page. With this approach, the main thing is to have a proper perspective of the mechanism for updating the client styles after changing the styles. The best way would be to generate a new hash and add it to the url, or add versioning for styles and indicate the current version in the same url (example – https://test.com/assets/themes/dark.css?v=3).
Apparently, both methods allow us to generate style files that will be available in all types and versions of the browser, while the backend can be implemented in any language and using any technology, including JS in Node.js.
It is rather difficult to consider options implemented in JS since many websites are implemented using JS libraries or frameworks, and others also contain built-in customization options or use some kind of libraries. For example, when working with the aforementioned Material in Angular, we will use the palette mechanism, and when working with Material-UI in React, we will use ThemeProvider.
import * as React from 'react';
import { ThemeProvider} from 'styled-components';
import Theme from './Theme';
export const Root = () => (
<>
<ThemeProvider theme={Theme}>
Put content here
</ThemeProvider>
</>
);
In order not to dive into the specific details of the implementations, I will mention a few solutions that I met when working with websites: style generation, variable substitution, and CSS-in-JS.
Generating styles for substitution and using them in the generated HTML through the inline styles is the simplest option, which has a major drawback: the logic of a custom theme begins to cover the entire application and is heavily embedded in its source code, thus violating many principles of pure development.
In order not to spread the code for generating styles all over the place, we can use a separate service that will store the state of the selected theme in the form of a set of variables and then use it in components to create inline styles or to generate a single CSS file. Apparently, generating inline styles will make the final HTML heavier, so I would not recommend this approach. In this case, generating a single style file manually is, in fact, an option that was mentioned above.
At the moment, the most interesting and popular solution for those who do not like working with CSS is the CSS-in-JS approach. Its main feature is that we can write all styles in the form of JS objects by freely substituting any settings both from the frontend and backend.
CSS-in-JS solutions generate the substitution classes themselves, so we don’t have to worry about typos or adding some extra style. The main problem with such solutions is that they are initially geared towards styling the internal components of the system, i.e. adding styles for embedded widgets or third-party components can cause some difficulties.
import jss from 'jss';
import preset from 'jss-preset-default';
jss.setup(preset());
const styles = {
'@global': {
body: {
color: 'green';
},
a: {
color: 'red';
}
},
button: {
color: 'blue';
}
}
const { classes } = jss.createStyleSheet(styles).attach();
document.body.innerHTML = `
<div>
<a href="/">Link</a>
<button>Button</button>
</div>
`;
Styled Components are another fairly popular solution that is closer to CSS. When applying styled components, we actually immediately bind styles to the component, thus giving the system the ability to override the state of these styles using JS.
When working with any of the JS solutions, we are free to customize our application according to our needs and build it for any browsers we are interested in, so there will be no problems with support in this case.
For quite a long time we have been working not with pure CSS, but with preprocessors, most often LESS or SCSS. The main advantage of preprocessors over pure CSS has long been mixin and variable mechanisms.
Mixins are actually the ability to generate a theme through substituting variables and using their values when inserting colors and other parameters. The use of a mixin is relevant if you store the values for different themes in different variables, and then, depending on the selected theme, you fill the mixin with the set that is expected in a particular theme. Although this option seems logical, it significantly increases the number of variables that we have to support, while the mixin itself can be quite heavy, or there will be too many mixins.
As a simpler solution, you can predefine a limited set of variables for a theme and use them in application styles, and then substitute other variables in these variables depending on the selected theme. This approach is convenient because we always have a limited number of colors. Therefore, there is less chance that we will end up with a bunch of shades of the same color. In addition, with such restrictions, we can integrate the design into the code and take styles based on some basic elements of the website.
Another interesting feature of preprocessors, in particular LESS, is the ability to substitute variables using code. In general, it is important to remember that in most cases preprocessors are used to compile into CSS at the build stage, so modifying the already prepared styles will belong more to the JS world. If you are using LESS directly as styles, you should pay attention to what styles you are uploading, try to separate the theme styles from the main styles and upload only what is directly related to the selected theme.
When working with SCSS, I would recommend using the preprocessor code in conjunction with native CSS variables, which will be described below.
Another option for working with styles is to use the PostCSS post-processor. Currently, there are several libraries available that support working with themes. I’m not going to go further into details since in most cases they are custom settings over applications and their configuration may be specific to some applications.
This option is the most recommended as it only uses native browser tools and is controlled by standards. The CSS customization option is based on CSS variables that are used by all modern browsers. It is important to note that this option will not work for those who need to support older browsers like IE.
Variables in CSS are similar to variables in preprocessors, but they have a specific syntax. The advantage of using this type of variable is that we can override values using JS: element.style.setProperty (“--main-color”, customTheme.mainColor);
Since the substitution of variables occurs in real time, we can substitute new values at the root level and they will be automatically applied throughout the application. If we don’t want to use JS to manually set the values, we can simply replace the style files that will replace the values of the variables.
Also, CSS variables support standard values, so we can always keep the default theme included directly in the styles of our website.
Any solution has its pros and cons, and they all require careful analysis before starting implementation. As is clear from the descriptions, the best solutions are definitely those that are close to native technologies, and you can always use them in conjunction with those tools that are already used in the project. If you need Internet Explorer support, I would recommend using a solution based on generating styles on the backend or implementation using preprocessors. In case IE support is not relevant for you, the best solution would be to use CSS variables.