šŸ‘©ā€šŸ’» chrismanbrown.gitlab.io

How to theme a website

a practical guide to site themes with a tiny bit of design system theory

2020-11-20

Contents

  1. Introduction
  2. Applications
  3. Choosing A Color Palette
  4. Creating Variables
  5. Building a bridge
  6. Theme Toggle
  7. Persisting, Like Elizabeth Warren
  8. Queries
  9. Conclusion

Introduction

Iā€™ve spent the last month or so working on a whitelabel e-commerce platform, the main features of which are:

  1. a well documented component library and design system, and

  2. being highly customizable through supposting custom themes.

Iā€™d like to use this this space to document and share the most basic elements of site theming using just CSS variables. That is, vanilla CSS and JavaScript, with no preprocessors.

Iā€™ll also share a little bit of design system theory.

Applications

Why would you care to read this?

Perhaps you too are developing a large customizable whitelabel e-commerce platform. But most likely you are not. Which is fine. After all, the overwhelming majority of us arenā€™t.

Perhaps you are merely curious.

Or, perhaps you would like provide your readers with an option between a dark theme and a light theme.

Perhaps you would even like to anticipate your readerā€™s preference through use of the prefers-color-scheme query, and deliver to them the color scheme of their preference.

Choosing A Color Palette

At the foundation of most design systems is the belief that there are things like colors and also typographic attributes such as font size and font weight that can and ought to exist in a vacuum. That is, they exist indepently of any page elements.

For example, you can define a type scale that as yet has nothing to do with your page titles. The scale just is. You can decide later which elements to apply the scale to. Maybe youā€™ll choose to only support three levels of headings, so you want a more dramatic scale, like that Golden Ratio over which certain nerds like nerd out so hard.1

Weā€™ll get more into the benefits of this in just a bit.

Let us focus for now on color, and let us define a color palette in a vacuum. One that has of yet nothing whatsoever to do with our site content or page elements.

Now we could do something predictable and grab a palette from somewhere like colourlovers, but why be such a normie pedestrian when we could do something fabulous and instead browse beyonce palettes for inspiration.

Hereā€™s one:

Thatā€™ll do nicely.

You can call these colors whatever you want. I think weā€™ll go with teal, blue, green, peach, pink, and purple.

Creating Variables

The first thing you really need to do is get your shit together and start using CSS variables, which are just like variables in other programming languages.2

They require you to use a specific --var-name syntax, and it is conventional to define these variables on the :root pseudoelement so that they cascade real good.

So if we were to define our beyonce color variables, itā€™d look a little something like this.

:root {
  --c-teal: #297077;
  --c-blue: #2DACB7;
  --c-green: #A1AE93;
  --c-peach: #DBB5AA;
  --c-pink: #D9A5BF;
  --c-purple: #AA58C3;
}

There. We have some color options which are now available throughout the document, and you can access them wherever you like by using var.

body {
  background-color: var(--c-teal);
  color: var(--c-pink);
}

var supports multiple fallbacks. So you can do something like:

background-color: var(--theme-background-color, var(--default-background-color), #333);

Building a bridge

An astute reader may recall that I said colors and typographic elements and whatnot can exist in a vacuum. And yet here I am doing a tight coupling of color directly to DOM elements. There is nothing at all vacuum-like about this!

Forgive me. I have over-simplified for the sake of demonstration.

In practice, a design system would ask you to create a mapping, a bridge, from a value to page element.

:root {
  /* colors */
  --c-teal: #297077;
  --c-blue: #2DACB7;
  --c-green: #A1AE93;
  --c-peach: #DBB5AA;
  --c-pink: #D9A5BF;
  --c-purple: #AA58C3;

  /* LOOK HERE! mappings */
  --page-background-color: var(--c-teal);
  --page-content-color: var(--c-pink);
}

body {
  background-color: var(--page-background-color);
  color: var(--page-content-color);
}

The reason I like this abstraction is for its simplicity and its semantics:

  1. Simplicity: I can now add, remove, tweak, or swap out colors at the color value level without worrying that much about how it will effect my DOM elements. I can adjust the brightness of ā€œpinkā€ in isolation without thinking about headers or links.

  2. Semantics: You can create more useful variable names now. Consider --c-danger and --c-warning. It is much more meaningful to set an elementā€™s color to one of those variable names than to the opaque #D9534F or even to the slightly better --c-red.

This mapping cordially escorts the designer from a simple list of available colors, to a more discreet and meaningful list of contexts. No longer do we merely have teal, blue, pink, green. Now we have the infinitely more useful --page-background-color and --page-title-color, and more.

This is the big idea behind design tokens:

Variables take the mystery out of obscure values. But they donā€™t bridge the gap between naming and use. They answer ā€œWhat options do I have?ā€ yet leave ā€œWhat choice do I make?ā€ unclear. A systemā€™s strength comes from knowing how to apply options (like $color-neutral-20) to contexts (like a conventional dark background color). This grounds the option as a decision.

Tokens in Design Systems, Nathan Curtis

Theme Toggle

This is a page theme toggle. (Caution: this will go from quite dark to quite bright!)

It uses the CSS variables that exist on this page, and the element.style.setProperty method to set them on the body element.

Were we designing a component, we would instead set the property on the component wrapper.

// kind of pseudocode incoming
forEach(radio => radio.addEventListener('change', evt => {
  const b = (evt.target.value === 'beyonce')

  body.style.setProperty('--background-color' , b ? beyonce.backgroundColor : 'var(--c-dark)')
  body.style.setProperty('--content-color'    , b ? beyonce.contentColor    : 'var(--c-light)')
  body.style.setProperty('--header-color'     , b ? beyonce.headerColor     : 'var(--c-primary)')
  body.style.setProperty('--link-color'       , b ? beyonce.linkColor       : 'var(--c-primary-variant)')
}));

Above, assume Iā€™ve already created a beyonce object of color values. And I also just happen to know that --c-dark & Co.Ā are default color values.

Persisting, Like Elizabeth Warren

Nevertheless, she persisted.

So the point of all this is that you can save and persist these color values. Perhaps, for example, in a JS module on diskā€¦

export default {
  '--theme-background-color' : '#DBB5AA',
  '--theme-content-color'    : '#AA58C3',
  '--theme-link-color'       : '#A1AE93',
  '--theme-header-color'     : '#297077',
}

ā€¦or maybe over a network call to a CMS or configuration server somewhere.

Or, as we will do here, in local storage.

Below is a theme editor that will, similar to the radio toggle above, change how this page looks, but with the added feature of allowing you to save your theme.

Invitation: Made a theme you like? Share it with me and Iā€™ll post it here for all to see!3

Note: To revert back to the default color scheme, delete the theme key from your local storage. Or, refresh this page and click save again. (The editor loads the default dark theme by default.)

Same concept here for updating the page:

inputs.addEventListener('change', evt => {
  const el = document.querySelector('.someParentElement');
  el.style.setProperty('--background-color', evt.target.value);
}

And we add a little splash of localStorage to the button click:

button.addEventListener('click', () => {
  const theme = {};

  // e.g. { name: '--background-color', selector: '#backgroundColor' }
  forEach(({ name, selector }) => {
    theme[name] = document.querySelector(selector).value
  });

  localStorage.setItem('theme', JSON.stringify(theme));
});

Now all I have to do is add a little javascript to the site header4 to load a theme object from localstorage.

document.addEventListener('DOMContentLoaded', e => {
  const body = document.querySelector('body');

  const theme = JSON.parse(localStorage.getItem('theme'));

  theme && Object.keys(theme).forEach(key => {
    body.style.setProperty(key, theme[key]);
  });
});

Queries

For completionā€™s sake, since I mentioned it at the beginning of this article, hereā€™s how you can query for a userā€™s color preference. Itā€™s just a standard media query just like any other!

@media (prefers-color-scheme: dark) {
  body {
    --background-color: var(--c-dark);
    --content-color: var(--c-light);
    --header-color: var(--c-primary);
    --link-color: var(--c-primary-variant);
  }
}

@media (prefers-color-scheme: light) {
  body {
    --background-color: var(--c-light);
    --content-color: var(--c-dark);
    --header-color: var(--c-primary);
    --link-color: var(--c-primary);
  }
}

Conclusion

Thatā€™s it. This is the end.

The main point of all this is this:

  1. List out all your style options in CSS variables: font sizes and font families, spacers (for margins and paddings), colors, shades, and tints. Etc.

  2. Create more CSS variables for your theme. Brand colors: primary, secondary, etc. At this tier you can assign the literal values to semantic contexts. E.g. ā€œheadings get brand color ā€˜tealā€™ā€.

Now youā€™re all set up to create sweeping changes to your page theme by changing the values of those semantic contexts.