According to data collected by Android Authority (2,514 respondents) and an analysis by Thomas Steiner (243 respondents), over 80% of users use a dark theme. Of course, it’s hard to call this sample entirely representative, since the surveys were conducted on technical forums, but overall, we can say that a good half of the internet uses a dark theme.
Every year, the web takes giant strides toward the bright future (or the dark one, depending on which you prefer). One by one, tools are adding dark themes, and major tech giants are updating and improving their design systems to stay relevant in this expanding dark world. Implementing a dark theme significantly improves the user experience and, as a result, business metrics. For example, Terra, one of Brazil’s largest news companies, recently increased the number of pages viewed per session by 170% and reduced its bounce rate by 60% after adding a dark theme [read the article].
The first part of the series was largely devoted to the history of CSS variables—their creation, development, and evolution - and also included examples of theming at both the planning and design stages as well as during front-end development, covering various methods of theming and theme switching [Theming: History, Reasons, Implementation]. In this article, taking it a step further, we will discuss client-server interaction and the capabilities of modern browsers in the context of theming.
Server-Side Rendering. SSR.
Before diving deep into server-side theming, it’s worth briefly touching on the topic of server-side rendering—what it is and how it works.
The history of server-side rendering began with the creation of PHP by Rasmus Lerdorf in 1994, just one year after the creation of HTML by Sir Timothy John Berners-Lee (also the creator of URI, URL, HTTP, HTML, and the World Wide Web [together with Robert Cailliau]). Despite the fact that PHP developed without a full-fledged specification until 2014, its popularity during those years was extremely high. In 2003, this wave of popularity was greatly boosted by WordPress, which to this day powers 40% of the internet.
In addition to PHP, other languages also attempted to fill the niche of a language for server-side rendering. For example, Java using servlets, or Ruby and its web application development framework Ruby on Rails. But they never managed to achieve any significant market share.
The turbulent era of PHP’s dominance began to wane in 2009 with the emergence of Node.js, and more precisely in 2010, when TJ Golovachuk wrote Express.js.
The next milestone began to take shape from 2010 to 2014, during the emergence of the “big three” - Angular (2010), React (2013), Vue (2014), which laid a solid foundation for a new type of web application - SPAs (single-page applications).
They all shared a common problem - the lack of any SEO optimization. Consequently, an equally significant trio of frameworks was subsequently created for them: Next.js (2014), Nuxt.js (2016), and NestJS (2017). These frameworks allowed applications to be generated on the server, thereby providing search engine crawlers with ready-to-index content.
SSR also has an advantage over SPAs in that:
- It supports Open Graph, thereby further improving SEO and adding previews on social media
- For most applications, FCP (First Contentful Paint) is significantly improved because the server response returns a fully rendered page immediately
- Users with JavaScript disabled will receive a fully functional page, and users with slow internet or low-end devices are more likely to see content within the first 3 seconds.
Server-side rendering has come a long way, just like other website rendering options.
Alternatives to SSR
Single-Page Application (SPA) - the very “big three.” The site is generated into JavaScript files, and all content is rendered on the client.
Static website. The website is generated into static pages, and these files are subsequently served from the server. The client immediately receives the finished page.
A hybrid of these two approaches is also popular, which is present in the previously mentioned Next.js and Nuxt.js out of the box - static site generation (SSG). With this approach, static HTML is served from the server, and a virtual DOM is reconstructed on the client so that the application subsequently runs in SPA mode.
The difference in the context of theming
Since rendering in an SPA occurs on the client—the definition of the theme and the addition of styles for it must reside at the top level of the application. Accordingly, all logic related to theme configuration must be moved into a separate bundle, or rendering must occur only after the theme has been defined. Additional complexities may arise in projects with a single source of truth, since all actions must pass through it, or in projects with styles stored in global objects (e.g., CSS-in-JS allows this object to be used within style functions).
In a static site, the theming logic must also be moved to the top level, but unlike an SPA, it does not need to render the entire page afterward. In both cases, the brief interval during which the user’s theme is determined will be accompanied by a color shift (if the user’s theme does not match the application’s default theme).
SSR, on the other hand, allows the server to render the correct theme immediately (based on the theme stored in cookies), and recently this has even been extended to new users.
Google: Theme Detection via Header
Previously, the only way to determine the user’s theme was to add a client-side script. Last August, with the release of version 93, Google added support for headers that allow the user’s device theme to be passed to the server. (The feature actually worked perfectly well in version 92 as well.) The functionality is based on client hints (dry standard documentation – https://datatracker.ietf.org/doc/html/rfc8942).
They allow the server to request the necessary user data. This data will be added to the request headers.
In the case of a device topic, the following headers must be added to the server's response:
Accept-CH: Sec-CH-Prefers-Color-Scheme
Sec-CH-Prefers-Contrast Vary: Sec-CH-Prefers-Color-Scheme
Critical-CH: Sec-CH-Prefers-Color-Scheme
The following header will be added to the request:
Sec-CH-Prefers-Color-Scheme: "dark"
Basically, it was only after the full implementation of this API that Google finally added a dark theme to Search.
Unfortunately, the hint functionality is only supported by browsers based on the Chromium engine. All other browsers (including Safari and Firefox) do not support hints.
Server-side theme implementation
The principles of class and style definitions, as well as client-side logic, are described in the previous article.
The following examples will be built using the Next.js framework. The exact same logic can be replicated on any other framework. The getServerSideProps function runs on the server and passes the return value to the page as props [more details].
We store the theme selected on the client in cookies.
const changeTheme = (newTheme: Theme) => {
document.cookie = `theme=${newTheme};path=/;max-age=31536000`;
// ...
};
First, we check if the user has a saved theme.
const cookieTheme = ctx.req.cookies.theme;
If the user’s theme is not saved, we determine the user’s theme based on the header.
const userDeviceTheme = ctx.req.headers['sec-ch-prefers-color-scheme'] as string;
If no theme is saved or an invalid theme is saved, we return the default theme.
const userDetectedTheme = cookieTheme || userDeviceTheme;
const defaultTheme = 'light';
const theme = (userDetectedTheme === 'light' || userDetectedTheme === 'dark') ? userDetectedTheme : defaultTheme;
We pass the user's theme to the client-side and use it during rendering.
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const userDeviceTheme = ctx.req.headers['sec-ch-prefers-color-scheme'] as string;
const cookieTheme = ctx.req.cookies.theme;
const userDetectedTheme = cookieTheme || userDeviceTheme;
const defaultTheme = 'light';
const theme = (userDetectedTheme === 'light' || userDetectedTheme === 'dark') ? userDetectedTheme : defaultTheme;
return ({
props: {
theme,
},
});
};
So, what should you choose - SPA, SSR, or SSG?
The choice is simplest with SPA - if you don’t have a server and search engine optimization isn’t a concern, this is exactly what you need. How to set up theming in a standard SPA is described in the previous article.
Between SSG and SSR, the choice depends on the following parameters:
- Is your content static (built once and never changes)?
- Does the content depend on a specific user (location, user agent, etc.)
- Do you need to perform a series of server-side operations before rendering (e.g., database queries)?
In the case of theming, the second point applies—different pages are rendered depending on the user’s theme. To be fair, this point alone is not sufficient to justify switching to SSR. It all depends on the server’s capabilities, its stability, and other objectives.
Additionally, SSG and SSR allow browsers to index the page correctly. Although Google can already index SPAs, it is still too early to call a single-page application SEO-friendly.
Other important aspects for evaluating a website are its speed and the user experience it provides.
Web Vitals is the standard for performance testing in today’s world. Therefore, let’s compare the metrics it provides for each mode.
The following examples will demonstrate three page rendering options:
- SSR on Next.js
- SSG on Next.js (note: this is not a static site in the traditional sense)
- SPA on React.js
All options are identical except for library-specific features (different root element IDs, different approaches to embedding in the head, and various additional capabilities).
The metrics listed below do not accurately reflect the actual performance of specific options. The examples are provided solely to demonstrate relative speed and user experience when interacting with the site in the context of theming.
First, let’s look at the metrics themselves
- When the default theme matches the user’s theme
- When the default theme does not match the user's theme
Now, about the reasons for this difference
In fact, the reports for SSR and SPA should be identical whether the themes match or not.
SPA is fully rendered on the client. Accordingly, it first determines the theme (in a fraction of a second) and only then begins to render the entire client-side portion with the correct theme.
SSG renders the page on the server; then, a virtual tree is built on the client and compared with the actual one. If there are no changes, nothing happens. If there are changes, the client-side is re-rendered.
SSR takes more time to render the page on the server, which increases page load time. On the server, if the theme header is supported, the page is rendered immediately with the appropriate theme.
Unfortunately, the emulator used in PageSpeed Insights does not contain any information about the device’s theme. If it did, the page would load immediately in the light theme. You can check and compare the results yourself; all links will be provided at the end of the article.
This can be seen more clearly in the logs
- When the default theme matches the user’s theme
Conclusion
This section should conclude which option to choose, but it would be presumptuous to provide a definitive answer. Lab tests can show which option is faster or more comfortable for the user (primarily due to the absence of flickering when switching themes), but it is impossible to predict exactly how the application will behave in specific projects. The only thing that can be said with certainty is that if you haven’t yet considered theming and you’re developing a web service, it would be useful to pay attention to this topic.
You can view the full code in the GitHub repository: https://github.com/alexdln/theming