Switch it up!

step-by-step tutorial: Theme switcher setup

Nowadays, our digital devices offer OS settings for light and dark view modes. The specifics of how exactly these controls should work are stirring up much discussion. How do we best accommodate our users, cater for their needs as well as their different personal preferences?

Should a website detect such device settings and adjust?

Or should the site not probe the device but offer the option to switch instead?

That is a bigger debate which has yet to find a good conclusion. This tutorial focuses on setting up a theme Switcher for an enhanced user experience, be that for the sake of better accessibility or for pure visual enjoyment and fun. We'll be using plain vanilla JS as well as local storage in this setup.

NOTE: The last switcher [colour + motion] is a demo of how too much colour, especially when combined with fast motion, can be detrimental and should be avoided. Read the Switch notes for more details.

Benefits of CSS switches

By definition, the CSS switch will give our users a choice of how to view our content. They might prefer less colour, or larger type size. Our fancy layout might be too busy for their taste. Our animated elements too distracting or right down annoying or even preventing some people from viewing our site.

Making no assumptions on taste or abilities, giving users options is something that can only be a good thing :) If our site's design is quite dark and moody yet presents lots to read, for example, then giving people the option to switch to dark text on light background will be welcomed by many.

Or our site's design uses a subtle colour scheme and very light font weights which is something that could be to the detriment of legibility and therefore accessibility. Offering people a choice will be useful to accommodate personal preferences as well as needs.

Switches can be implemented in different ways. In the past, one solution used alternate stylesheets, using JavaSript (JS) to prompt the browser to load and apply it. This method did not catch on and so approaches have evolved in different ways. The design changes could be written into the main CSS file, for example ~ and the body class changed via JS, triggering those changes to be applied.

This tutorial will implement the switch function with JS, use different CSS files and ensure persistent behaviour via local storage. Persistent behaviour means that the loaded styles, once applied, will remain unchanged if the page is reloaded and will be used for other pages of the website as well. This could be done by setting cookies - which was a common approach in the past. However, this method would immediately require measures for data security (and inclusion of details for GDPR). Using local storage instead is therefore a more modern, cleaner and less complex approach.

the workings

Our page will have one default CSS which will include all settings for design and layout. Additional CSS files are created to implement the themes which will include overrides for their respective versions.

illustration of CSS loading via browserEssentially, the page will always load the default CSS (with the core settings for our design). The additional theme CSS file will be loaded when the switch is used.

default CSS

We are using one main CSS for our site which is already linked to from within the <head> tag. And this is the big one, i.e. the default CSS which holds all design settings. It's called the persistent stylesheet and used by default; the theme CSS is applied in addition.

<link rel="stylesheet" href="css/style.css">

theme CSS

For the different design version, we'll now create new CSS files which will contain rules to make certain changes to the already implemented design. We might only have two CSS files for the light/dark views. Or add an additional one for a high contrast version. The theme style sheet will be loaded in addition to the default CSS ~ usually hold less code as it will rely on the already applied settings and merely include overrides.

the switch

From a design perspective, the switch can take any shape, a sliding button or simple text. It's best to follow the project's UI patterns and consider how this function will fit and tie in its visual style with the overall design.

From a development perspective and considering the user experience, we know that this switcher will rely on JavaScript to work - it is therefore a progressive enhancement [see MDN for details]. To avoid confusions and frustrations, we will therefore not show the switcher links at all if JavaScript is not available (as these would not work and appear broken).

Instead, the switcher will only be added to the page when JS is available and our markup will merely include an empty container (with a specific ID) in our chosen location within the page (usually the header). On page load, JS will then add the switcher and enable its functions.

<div id="themeselect"></div>

The JavaScript functions themselves will be added to a separate file and loaded via a link in our markup, as last item before the closing </body> tag.

<!-- JS -->
<script src="js/theme-switcher.js"></script>

One of the easiest accessibility enhancements is to set up a theme switcher for the high contrast version. This design will focus primarily on setting a strong contrast between text and background colour to increase legibility. Typically, a high contrast design will set a dark background with bright text and adjust typesetting accordingly.

While designed with accessibility in mind for people with low vision, these versions are also popular, and often preferred, in certain settings (e.g. when in a cinema, aiming to keep light emissions low) or when concentration is lagging. A great addition to most sites ~ let's take this as the example to set up the theme switcher as shown on this page.

project folder

screenshot of folder structure

This page's final set up is as follows:

#1 finalise main CSS

This is the master CSS, the big one already included in our website - added inside the <head> tag:

<link rel="stylesheet" href="css/style.css" />

Note: this is the only link added to our markup and it includes the full set of rules for our design: typesetting, colour and layout. The themes are additional files which will be added via JS in the following steps.

Before working on any alternatives to our design, let's check our CSS thoroughly and make sure we're happy. As our plan is to change colours, it will be best to ensure that colour rules set are minimal, as few as possible, making the most of the cascade and allowing for overrides with few rules.

#2 create theme CSS

We'll save a new, empty file into our CSS folder, naming it style-theme1.css. To add more themes later on, we'll use the same name, only changing their numbers in sequence. For easy workflow, it is good practice to add a comment at the very top of the CSS to outline the design specifics :)

In our case we're adding a dark high contrast version so our comment might read:

/* theme 1 - dark version of design, high contrast with full colour */

workflow considerations

We will always load the default CSS first as it implements the page's design. In addition, making full use of the cascade - our theme CSS will follow and serve the purpose of changing only select aspects of this design, e.g. override the colour settings for a different appearance with higher contrast and adjust typesetting accordingly.

NOTE: the order in which to list the CSS files has to be correct in order for this to work. Style rules get implemented strictly from top to bottom. The default CSS has to precede our theme CSS.
To work on this theme's overrides, we have two options:

option 1: temporary addition of link to CSS

Add the new CSS into the <head> tag, following the default CSS. This will load both CSS and we can work on our overrides easily. When done, we then remove this second link, ready for the switching function.

<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/style-theme1.css" />
screenshot of folder structure

option 2: alternate CSS in FireFox

If we want to switch between the default and the theme CSS files in the browser for quick comparison, we can use the method mentioned previously: declaring the theme CSS as alternate stylesheets and giving them a <title> attribute. Firefox will allow us to switch easily between all linked CSS via top menu > View > Page Style

<link rel="stylesheet" href="css/style.css" />
<link rel="alternate stylesheet" title="theme1 - dark" href="css/style-theme1.css" />

NOTE: this is a very quick and easy way of working on different design themes. To make this work, we'll need to add alternate to the <rel> attribute as well as a fitting <title> attribute. And as with the first option, once the CSS edits are done, these extra CSS links will be removed from markup, ready for the theme switcher functions to do the work instead.

#3 add JS functions

As you might (or might not) know, I don't write JS and will happily stick with HTML and CSS. For progressive enhancement features, I rely on the sharing spirit of others whose coding skills are more advanced than mine. This tutorial originally made use of a classic from A List Apart: Alternative Style: Working With Alternate Style Sheets by Paul Sowden, 2001. The article is quite old by now but it's still one of the most direct and clear tutorials on the subject, and its method is still working perfectly. However, as this approach relied on alternate stylesheets (which is now only supported by Firefox) and also on cookies to be set - the new method of using local storage is both more modern and overall a smarter approach.

Thanks to David Watson, this tutorial has been updated and is now inline with new techniques. David has written an excellent explainer article on this as well, highly recommended reading. For brevity's sake, the snippet included here shows only minimal comments, do make sure to check out David's article for details and in-depth explanations.

// Theme Switcher v1.4 by David Watson
// Add the names of themes
const themeNames = ['Default', 'Dark', 'Black & White', 'Large/Fancy', 'Colour + Motion'];
// The path to our CSS theme files.
const path = "css/";
// Count the number of persistent stylesheets in the current document
const ssnum = document.querySelectorAll('link[rel="stylesheet"]').length;
const theme = localStorage.getItem('theme'); if (theme !== null && theme !== 'default') createLink(theme);
const fieldset = document.createElement('fieldset'); document.querySelector('#themeselect').prepend(fieldset);
let i = 0; // Set counter do { let input = document.createElement('input'); let label = document.createElement('label'); input.setAttribute('type', 'radio'); input.setAttribute('name', 'stylesheet'); if (i === 0) { input.setAttribute('id', 'default'); input.setAttribute('value', 'default'); input.checked = true; label.setAttribute('for', 'default'); }else{ input.setAttribute('id', 'theme' + i); input.setAttribute('value', 'theme' + i); label.setAttribute('for', 'theme' + i); if (theme === 'theme' + i){ input.checked = true; } } label.textContent = themeNames[i];
// Add label text
fieldset.appendChild(input); fieldset.appendChild(label); i += 1;
// Increment counter
} while (i < themeNames.length);
// Keep going while there are theme names
fieldset.addEventListener('change', (event) => { let selectedTheme = event.target.value; let stylesheets = document.querySelectorAll('link[rel="stylesheet"]'); if (stylesheets.length > ssnum && selectedTheme === "default") document.querySelector('head').removeChild(stylesheets[ssnum]); else if (stylesheets.length > ssnum && selectedTheme !== "default") stylesheets[ssnum].setAttribute('href', path + 'style-' + selectedTheme + '.css'); else createLink(selectedTheme); localStorage.setItem('theme', selectedTheme); });
function createLink(theme) { let link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('href', path + 'style-' + theme + '.css'); document.querySelector('head').appendChild(link); }

Name your themes!

Looking closely at the JS file, we can see a clear prompt for the one edit that will have to made to get this script to work in our setup:

// Add the names of themes
const themeNames = ['Default', 'Dark', 'Black & White', 'Large/Fancy', 'Colour + Motion'];

Starting with 'Default' - we will list the names of our themes, separated by commas. The order of names will reflect the numbering of our themes' CSS files - the text itself will be shown alongside each switch on the web page itself.

Link to JS theme functions (theme-switcher.js)

We'll now save this file into the 'js' folder in our site's directory and link to it from our pages. Traditionally, this script tag is added just before the closing </body> tag. These days, there are many other methods ~ make sure to check on other dependencies and scripts of your project and use the methods that fit.

<!-- JS -->
<script src="js/theme-switcher.js"></script>

#4 style your switches

As final step the newly added elements will need some TLC :) ~ we'll now test the switcher via the radio buttons and check that all our themes are displaying correctly. With all in working order, we'll work on the appearance of our theme switcher.

We could opt to keep the radio buttons but as those are typically found in forms and not very nice to look at ~ we will hide them from view and style our switchers via their labels instead. The specific CSS will of course be tied to the project in hand and its design. The following is the CSS used for this page.

We'll initially target the empty div via its designated ID (#themeselect) for position and style of the main element. Next, we'll target the newly added fieldset to control layout/size/position. And finally, we'll hide the inputs — and finish off with styling the label elements which contain our theme names.

/* theme switchers */
#themeselect {
	padding: 0 0 24px 0;
	font-size: 1.24em;
	margin: 0 auto;
	background: #3daac2 url(../vis/lines1.svg) top left repeat;
	background-size: 20px;
/* layout/size/position of switch container */
#themeselect fieldset {
	text-align: left;
	width: 80%;
	max-width: 960px;
	margin: 0 auto;
/* hide radio button input */
#themeselect fieldset input {
	display: none;
/* style label */
#themeselect fieldset input + label {
	display: inline-block;
	margin: 12px 2vw 0 0;
	border: 2px solid rgba(255,255,255,0);
	position: relative;
	text-decoration: none;
	white-space: nowrap;
	padding: .36em .64em;
	text-align: center;
	color: #fff;
	text-transform: uppercase;
	font-size: 0.75em;
	background: #5F3C94;
	-webkit-transition: all 0.22s cubic-bezier(0.23, 1, 0.32, 1);
	-moz-transition: all 0.22s cubic-bezier(0.23, 1, 0.32, 1);
	transition: all 0.22s cubic-bezier(0.23, 1, 0.32, 1);
	box-shadow: #007792 0.04em 0.04em 0, #007792 0.08em 0.08em 0, #007792 0.12em 0.12em 0, #007792 0.16em 0.16em 0, #007792 0.2em 0.2em 0, #007792 0.24em 0.24em 0, #007792 0.28em 0.28em 0, #007792 0.32em 0.32em 0, #007792 0.36em 0.36em 0, #007792 0.4em 0.4em 0, #007792 0.44em 0.44em 0, #007792 0.48em 0.48em 0;
#themeselect fieldset input:hover + label {
	border: 2px solid rgba(255,255,255,.5);
	background: #007892;
#themeselect fieldset input:active + label {
	-webkit-transform: translate(0.48em, 0.48em);
	-moz-transform: translate(0.48em, 0.48em);
	-ms-transform: translate(0.48em, 0.48em);
	-o-transform: translate(0.48em, 0.48em);
	transform: translate(0.48em, 0.48em);
	box-shadow: none;
	text-shadow: none;
	background: #19a4c3;
/* show currently active switch */
#themeselect fieldset input:checked + label {
	border: 2px solid rgba(255,255,255,.5);

This concludes our setup — and next it's onto testing, final fixes and tweaks.

In summary:

Quick recap of the process of our theme switcher setup:

  1. finalise main CSS

    Before adding any theme options, the HTML and CSS are completed to implement the final design of the page/site. The CSS includes all core settings - typesetting, colour scheme and layout are in place.

    screenshot of folder structure
  2. create theme CSS

    With design complete, the theme CSS is created. Saved as new file into the same folder as the default styles, this CSS now includes only the overrides necessary to change the select aspects of the design as required.

    screenshot of folder structure
  3. add JS functions
    1. ensure only the default CSS is linked in HTML <head> tag:

      None of CSS files for the themes are to be included; these will be activated via JS only.

      <link rel="stylesheet" href="css/style.css">
    2. ensure correct naming of theme files in CSS folder:

      The theme names have to begin with style-, followed by theme# (unique numbers starting with 1); all theme CSS to be located in the same folder as the default CSS file.

      default: style.css

      additional themes: style-theme1.css
      style-theme2.css, style-theme3.css, etc

    3. include switcher div in HTML:

      The placement of this empty div will be in the location where the switcher elements is to be shown.

      <div id="themeselect"></div>
    4. edit theme names in theme-switcher.js

      These are the names which will be displayed on the page and which will act as switchers.

      // Add the names of themes
      const themeNames = ['Default', 'Dark', 'Black & White', 'Large/Fancy', 'Colour + Motion'];
    5. link to .js file in HTML:

      This will load the JS file if JavaScript is available in the browser and will activate all functions.

      <!-- JS -->
      <script src="js/theme-switcher.js"></script>
    screenshot of folder structure
  4. style the switches:

    As final step - we'll add a few more rules to our default style.css to integrate the switches into the design. The rules in theme CSS will also need to be checked, updated and added to as required.

About the featured switches

  1. default (light) the default design version: dark set text on light background; use of 2 core colours for UI, headings and highlights.
  2. dark the dark version of the design: reverses colours and sets light text on dark background; colours for UI, headings and highlights edited for lighter tone; typesetting updated to adjust size and line height for legibility.
  3. black & white the desaturated black & white design version: strips the design of all colour, using black/white and shades only; typesetting updated to adjust size and line height for legibility; presentation of media (image/video) uses CSS greyscale filter to desaturate all colour.
  4. large/fancy the large, bold design version: the most elaborate of the designs adds detail to background; changes to typefaces; typesize is responsive, scaling up with increasing viewport size.
  5. colour + motion for demo purposes of the effect of colour/motion ~ exaggerated settings to show detrimental effect of gradients and animation to legibility.
    As some people will be sensitive to motion, this switch includes a tooltip notice as a warning message. It will state the effect of the theme before it is activated.

An example switcher for light/dark modes

The use of the these two different viewing modes has now become popular and widely used. This tutorial shows a number of switches for demonstration purposes and using text is the most effective way of stating the given options. A typical website, however, might only have two options in which case icons might work best.

static demo

  • light mode
  • dark mode

note: this is a purely visual demo without functions This examples shows how an icon-based switch might look. We could hide all text and use icons alone, adding a tooltip for clarity. In this case, two version of the icon are included for the background image: PNG for fallback and SVG.

A note on the 'colour + motion' version

Animated elements have long become a popular trend in webdesign. And there are certainly cases when motion will add visual flair and even enhance the understanding of content (e.g. in tutorials or product demos). However, this is very subjective ~ similar to colour. Some people will love it and appreciate the added visual embellishment. While others will dislike it and might even be struggling with it.

In the worst case, some people will find the effects of motion so bad that they will physically feel sick. This is a serious issue and has now been addressed by the prefers-reduced-motion CSS media feature [see MDN for details]. This will respect the user preferences (set on their device of app) and aim to disable overly animated features. An important issue to keep in mind before we add any kind of animated presentation to our websites.

colour splash! only when desired :)

With theme switchers we now have the ability to still add our motion effects without affecting anyone negatively. We can present the static version in the first instance, and only add motion for those who choose it ~ easy :)
This is why the last switcher of this tutorial only introduces the colour+motion version via a switch on click. To ensure those who prefer reduced motion are aware of the upcoming animated effect, the warning message has been added. This version uses the same effect as the following example - the animation effect has been set to a much faster speed to show how detrimental excessive colour and motion can be to legibility and the reading experience — i.e. entirely over the top, overload!!

static demo

note: this is a purely visual demo without functions In this case, we might only have a very simple function. A single option to enable or disable the animation element - a single visual instance will suffice and show a clear on/off switch.

An example

Let me give you one good example of when a CSS switch is useful to allow possible annoyances to be removed. This is a single webpage which is loaded with colour via an animated gradient background. Something which some will love - and some will absolutely hate. An 'off' switch was the only way :)

The example is the addendum page for small site I created with the main purpose of introducing web experts to my web students and anyone interested :) It's an informal little listing of people and I used colour mainly as an added touch, to hint at areas of expertise. I created a supplementary page to elaborate on this concept and felt like going all out on colour for this page alone. As this is primarily for my students, I felt it was ok to add this for the sheer fun of it (and to serve as demo, too - it still uses the old method for the switcher functions).

Take a look at the page: People of the web - Addendum.

The animated gradient (otherwise reserved for smaller elements on the site only) is applied to the background of the entire page. This is a drastic change to the feel of the site. It delivers a splash of colour which then slowly moves, gradually changing colour cycling through the different tones. Text is set in white and is clearly legible overall.

This might be fine for most people while the movement of colour alone might hinder legibility for others, not ideal. Most affected are the large words set in different colours. The changing colour combinations play games with your eyes as the shades used for the text make it appear to fade in and out of visibility depending on background colour passing by. A typical scenario when one size does not fit all and offering a 'way out' via stripping the colour will be best.

To allow people to get rid of the colour in motion, I've added a CSS switch which removes the background colour and changes the text to dark. The page is now presented in keeping with the rest of the site and with consistency restored, hopefully everyone will be happy :)

With the mixed content, the moving colours quickly become tiresome and slow down reading. Concentration fades. This shows that overly bold design touches like colour and motion have to be handled with care, applied only when fitting context and audience. In the case of this tutorial, a colour effect like this would be counter-productive and the worst idea :D

In comparison, the addendum page example consists of only text which is set at a fairly large size to maintain legibility on a busy or bold background. This might not be to everyone's taste but the combination of white text and the slow speed of the motion presents a readable page at least.

Closing thoughts

Adding a CSS switch can bring so many advantages, can be a ton of fun and is definitely something we should consider and keep in mind. As we saw, it was not very complicated nor took long to set up switches for versions which had a different purpose and feel. These alternative version did not require many additions to change our page's design quite drastically and are well worth the time and effort.

Here are a few things to keep in mind if you're planning to add alternative versions.

design demo © eyesrc.org