DEV Community

Daniel Del Core
Daniel Del Core

Posted on • Edited on

An approach to theming Design Systems

To create a theme that will scale with the needs of your organisation, you must first choose the set of rules and principles that contribute to a scalable, flexible and sane theming solution. These rules need to be incorporated into a specification which will eventually need to be supported as a first-class API contract between your Design System and consumers.

A theme can be thought of as a set of variables or 'global tokens' that are applied globally to your components. Containing all of the colours, spacing units and typography rules. Together they define how your components look and feel (not their behaviour). Allowing you to provide tailored and accessible experiences via dark mode, high contrast mode and more. 

Themes need to conform to some form of 'Theme specification', a schema which describes the structure, naming conventions, properties, data types, and scales of our theme. The definition of this schema carries hidden long-term implications, so it's important to define these rules with care. Let's walk through a some solutions you can put in place to navigate these complexities.

Theming is not the silver bullet for customisation

Firstly, I want to call out that customisation is tricky. There are many ways to achieve it and all come with a healthy amount of pros and cons. It's a matter of using the right tool for the job. Global Theming is no exception, it's a great tool for rapidly and consistently effect change to colours and spacing and seeing immediate results, but it does not have any way to express component behaviour. Nor should it be expected to turn your Design System into a completely white-label suite of components. There are just too many tiny decisions baked into every aspect modern components, it would simply be next to impossible to control every case.

Instead, think of a theme as the broad brushstrokes of customisation. Almost always used alongside other techniques such as Props, Composition, Overrides, CSS hacks etc.

Setting the expectations of what theming is and what it is not is important to ensure the solution isn’t carrying too much of a burden, which is often followed by degraded performance and increased cognitive overhead. In short, doing less is always best (ha).

Most Design Systems offer multiple avenues for customisation, but the decision making should be in the hands of users to choose the right approach for them.

Themes are a reflection of UI anatomy

Consider the following example of a theme:

// theme.js
export default {
  colors: {
    transparent: "transparent",
    black: "#000",
    white: "#fff",
    gray: {
      light: "#eeeeee",
      dark: "#202633",
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Each colour defined here describes a value not a destination. What if you want to invert black and white in your UI to create a dark theme? You might naively switch the values, but then quickly realise that the names become a lie. Black is white and white is black.

Moreover, consumers of the theme might be using these colours in different ways. gray.light might be a great background colour to one developer, but also might already be used as a background by secondary buttons. Eventually, someone will want to tweak the theme’s background colour (gray.light) which might then break colour contrast for secondary buttons. There’s nothing stopping these values from being inadvertently mixed up, causing wide-reaching issues. Remember we are operating in the global scope 🌏.

human skeleton diagram

Similar to how every ridge, bump and groove of a human skeleton 💀 has been analysed, named and catalogued, so should every facet of our UI “anatomy”, if you will. That catalogue is the Theme, which describes the relationship between spacing, colours, focus rings, typography, etc have with the particular ridges and grooves of our UI, Buttons, Navigation, Forms...

So with that in mind, the theme above might be better represented in the form:

// theme.js
export default {
  colors: {
    typography: {
      body: '#000',
      heading: '#393939',
      anchor: '#439dfa',
    },
    background: {
      body: '#eee',
      aside: '#e2e8f0',
      footer: '#e2e8f0',      
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

As you can see some of the primary elements of the Holy grail are listed here. These concepts are abstract in meaning and don’t refer to any specific component, they are simply parts of a typical webpage’s anatomy that you, the designer or developer, are already familiar with. These “concepts” are what I believe is the key to encoding semantic and contextual meaning into a theme.

Theme properties should have contextual & semantic meaning

Through describing where properties are intended to be used, rather than the value they hold, via abstract concepts, users of the theme should find it intuitive and easy to use the correct properties in the correct place.

Given the examples above, consider a user wants to ensure some text on the page is using the correct typography colour, the structure and naming of the theme describes to them the best fit.

Before:

const CustomHeading = styled.h1`
  font-size: 3rem;
  color: ${(theme) => theme.colors.black};
`;
Enter fullscreen mode Exit fullscreen mode

After:

const CustomHeading = styled.h1`
  font-size: 3rem;
  color: ${theme => theme.colors.typography.heading};
`;
Enter fullscreen mode Exit fullscreen mode

Hierarchy integrates our intention for each theme property into the structure of the theme. It’s clear that for typography colours, it’s probably best to use color.typography.element and so, if I decide to use a background colour to style some text, I’m probably doing something wrong and it's going to be prone to breaking sometime in the future.

It’s also worth noting that Material-UI already does a great job of using hierarchy to group properties into significant elements.

screenshot of material-ui theme hierarchy

Values making up the theme should be restricted to ‘Brand Tokens'

Values that populate our theme should be limited to the set of 'Brand Tokens' for use within your organisation, so whichever combination of values selected from these tokens, the end result will still be distinctly on brand.

Not all brand tokens need to be in use for any given theme. They’re just the available “pool” of options for you to choose from. For extreme use-cases it’s still possible to substitute your own values.

Again, taking the example from above, moving the values into ‘Brand tokens’ allows us to apply colours consistently without picking arbitrary values.

// tokens.js
const neutral50 = '#eee';
const neutral100 = '#e2e8f0';
const neutral500 = '#393939';
const neutral600 = '#000';

const blue100 = '#439dfa';
const blue200 = '#439ddd';

// theme.js
export default {
  colors: {
    typography: {
      body: neutral600,
      heading: neutral500,
      anchor: blue100,
    },
    background: {
      body: neutral50,
      aside: neutral100,
      footer: neutral100,      
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

In a dark mode theme, you could simply replace the lighter tokens with darker ones whilst still conforming to your Brand.

// theme-dark-mode.js
export default {
  colors: {
    typography: {
      body: neutral50,
      heading: neutral50,
      anchors: blue200,
    },
    background: {
      body: neutral600,
      aside: neutral500,
      footer: neutral600,      
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

Scales need to defined and consistently applied

If I use T-shirt sizing can I scale it to suit my needs in the future? Will an array carry enough semantics for others to understand? How can I describe progression of colour?

No doubt you’ve already done these mental gymnastics when trying to rationalise a scale or a group of related values. Choosing the right scales can be hard. They need to be consistent, flexible and hold some form of semantic value. The right system also needs to be paired with the right type of value, e.g. T-Shirt sizes can’t describe colours. It’s a tricky problem and it imposes a large obstacle to the flexibility and longevity of your theme.

As you would expect there has already been plenty of thought on this, so i’ll point you to this wonderful blog incase you wanted to go into more detail.

I’ve seen a handful of scales used in themes in the wild, here are a few popular ones:

  • T-Shirt sizing: xxs, xs, s, m, l,xl, xxl, xxxl, xxxxl
  • Arrays: fontSizes: [14, 16, 18, 24, 32]
  • Enumeration: b100, b200, b300
  • Degrees of x: lighter, light, lightest, lightestest…

Like everything, all have pros & cons, but to offer up an alternate opinion. What if we could extend on the thinking above and use that same thought process to side step the scales conversation?

Consider spacing, you have an obvious set of options for defining a scale of spaces, you might use an array or T-Shirt sizes. But those values are still subjective, some people might use space.m where others use space.s it’s still largely open to interpretation. What if every value in the spacing scale had a finite set of use-cases and those use-cases could be encoded into the name, the same way we did with colours above?

Stealing an image from this blog by Nathan Curtis (definitely worth a read).

Spacing types diagram

We could use those concepts to define a new spacing scale:

// tokens.js
const space50 = '0.5rem';
const space100 = '1rem';
const space200 = '2rem';
const space300 = '3rem';
const space400 = '4rem';
const space500 = '5rem';

// theme.js
export default {
  colors: {...},
  space: {
   inset: space200,
   inline: space50,
   squishedInset: `${space100} ${space50}`,
   stack: space400
  }
};
Enter fullscreen mode Exit fullscreen mode

Now when choosing a spacing value for a new container component, we can take the guesswork out of the process, since we know that space.inset is primarily used for padding the inside of containing elements.

Most collaborators can’t see space, a primary reason it’s so arbitrarily applied. But now we’ve got a system: a limited number of concepts, each offering a limited range of options.

But weight (hehe get it?) there’s more, since one inset is never going to be enough, now is the perfect time to introduce a scale to provide more granular control within the safe boundaries of a meaningful naming convention.

// tokens.js
const space50 = '0.5rem';
const space100 = '1rem';
const space200 = '2rem';
const space300 = '3rem';
const space400 = '4rem';
const space500 = '5rem';

// theme.js
export default {
  colors: {...},
  space: {
   inset: {
    default: space200,
    small: space50,
    medium: space300,
    large: space400,
   },
   ...
  }
};
Enter fullscreen mode Exit fullscreen mode

In this way, the spacing scale is not as inclined to grow organically into a huge unmanageable array of values as new values are eventually added, making the system more resilient to change over time.

A Theme’s concepts should be well documented

Lastly, it is not enough to have a great theme, it needs good documentation because when used incorrectly it has the potential to destroy its value and lock API maintainers into a lifetime of breaking changes and workarounds.

With all of these abstract concepts in place like “scales”, “inset”, “squish”, “token”, there needs to be a somewhere people can go to make sense of it all. So again, i’ll point you to this blog:

Teach your spatial concepts using a tight doc diagram or cheat sheet. Such references quicken how we grasp, apply, and sustain the concepts through design and code.

spacing doc diagram

And better yet, visual prototyping tools can bring the docs to life by giving users a feel for what they’re changing as they’re changing it. And the ability alter UI in broad brushstrokes 🖌.

The ecosystem has already made a headstart on this:

With good documentation in place, a greater overall understanding of how the theme should be used can form within your organisation. Future changes to the theme, as it grows organically overtime, fit into the already understood frameworks and paradigms we’ve defined (and documented). New themes can be designed, published and shared easily between products, teams and users. And limitations of a theme for customisation purposes become more apparent, prompting people to take more appropriate avenues to customisation problems they’re trying to solve.

Thanks for coming 👋

There are many ways to skin the big fat theming cat 😺, so treat this blog as merely a set of observations and ideas you could potentially incorporate into your Design System.

Please feel free to post your ideas and opinions below, i’d love to hear them!

Top comments (2)

Collapse
 
gossi profile image
Thomas Gossmann

Thanks, was good reading about the tokens here - because I did something very similar (just named it differently - haha ;). I made some public libraries available for figma and tools to sync them to your code. Have a look:

Collapse
 
gayanhewa profile image
Gayan Hewa

Good stuff DDC