Since we started NewsKit we’ve learnt a thing or two about theming, in this article we will go through different approaches, discuss associated trade-offs and how we’ve solved these challenges. It’s fair to say we were a little naive on how complex building a multi-brand design system would be when we started this journey! If you’re embarking on a design system project or looking to adopt NewsKit in your teams this article should help to get you up to speed on the different approaches available.
Theming is a core cross-cutting concern of a multi-brand design system, without a standardised approach, we would end up with a collection of components with potentially very little in common. It’s taken many iterations to get to where we have, whilst the journey isn’t over, we believe we do have a solid foundation in place to support the growing requirements for our diverse set of brands.
A bit of context
Using a simplified version of our button as an example let’s looks at how this can be styled and the trade-offs associated with each approach
Imagine a webpage that only makes use of Inline styling.
First of all, it’s not that easy to read, and that is with a heavily redacted set of attributes. If we have multiple buttons on a page, it becomes a pain to update them all, it’s easy to miss an instance, even more so when we have multiple pages. We are quickly going to end up with inconsistencies across our UI, damaging product fidelity and brand perception. No sane person will want to maintain this at any scale.
A solution to this is to use CSS classes that can be shared across our site.
If our team is disciplined and uses the correct button class on every element we can easily update all the buttons in the application from a single place. CSS Zen Garden is the definitive example of how powerful this can be.
However as our system evolves we’ll want to introduce new button types, such as our outline button above. This button shares many attributes with our default button. The problem we run into now is if we want to change our primary button colour we need to find the value and update multiple places in our CSS, not as bad as everything being inline, but still a pain and potentially difficult to manage in larger solutions that have many components relying on the same values. CSS variables allow us to sort this mess out, we can centralise the important parts of the theme in these variable declarations to build a simple theme.
The important takeaway here is the manner in which we organise relationships between these variables and classes can have a dramatic impact on the future maintainability and consistency of our application. In the design system world, the parlance for these variables is design tokens. These tokens can be a mapping to raw values like above, a mapping to another token, or an aggregation of several tokens, more on this later.
Over the last few years, CSS in JS libraries have continued to gain traction, it’s outside the scope of this article to discuss all the associated trade-offs, but what’s important to us is it allows us to componentize our CSS and makes it easy to get render critical CSS out of the box, which in the world of publishing is important not only for our readers but also it’s increasing impact on search rank with Googles incoming CLS changes. It should be highlighted that the theming concepts described herein should be possible with vanilla CSS but they will be much harder to manage.
Initial Theming solution
In the very early days of NewsKit, our button was similar to the following. We have helper functions getting values from the theme. The
getTypePresetFromTheme function gets several CSS attributes at once.
This has a few issues.
- Not all values are tokens, not everyone wants an 8px border, this is however easily fixed by just tokenising additional values. There is however a cost to tokenising each new property as a scale should be defined for the underlying value range.
- Our initial intention was to rely on the emotion theme provider to change the tokenised values. However, creating a new theme each time you need a new button style is tedious, and managing that many themes will become an issue in itself.
- Values that needed to be overridden inline could be done by exposing additional props, the problem with this is your component interfaces quickly become very cluttered. Additionally, it’s hard to predict how consumers are going to use the component, deciding what belongs on the component quickly becomes guesswork.
This is mostly an example of a contextual theme, by this we mean our UI is going to behave differently depending on where it is placed, We can develop features such as dark mode or sub-brands on the same page provided we tokenise the attributes.
It’s worth noting that for many organisations this contextual theme is all they will need. However, we have a lot of different brands with varied requirements, overriding components at both an individual level and theme level in a consistent manner is required.
Expanding Preset Usage
As we saw above, we grouped typographic styles in a single group, which we refer to as presets. Essentially a preset is just a group or an object of CSS properties that can be reused through reference to a token. This grouping makes it easier for consumers as our APIs become more consistent. It also helps us by meaning we don’t have to anticipate every single property a consumer might want to bring to a component.
At the time of writing, we have the following presets in NewsKit
- Typographic: where text styles live, headlines, subheadings, paragraphs, etc.
- Style: colour, backgrounds, borders. It’s worth noting that these support pseudo-states such as hover, active, etc. in addition to custom ones such as valid and selected.
- Space: provides padding and margins.
Adding presets to our button example we get the following.
A problem that our button example doesn’t really make apparent, is even with presets, we end up with a significant amount of properties on our components; take our audio player for example.
This is exposed to the developer as a single component, yet it’s constructed of buttons, sliders, and text elements. Giving our consumers access to change each of these underlying elements is going to require introducing a significant amount of props onto the root of the component. Whilst this could feasibly work it’s not a great experience to be overwhelmed with theming options for nested elements. It also has a maintainability issue because each time we introduce a property on the button we would need to introduce it on the audio player as well, discrepancies will slip in.
The solution to this is simple, we have single overrides prop on each component that is used to override the theme for that instance, this can be thought of as being equivalent to inline styling. The object structure can be nested meaning we can extend indefinitely. If we add new props on an underlying component we get this for free on the parent because it will be passed straight through.
Our code ends up looking like so:
One thing overrides don’t allow us to do easily is modify all our button instances from a single location. This is where component defaults come in, in a way they are very similar to overrides but they are applied at a theme level, conceptually similar to updating our button class in the earlier CSS examples.
Every component in NewsKit comes with defaults that are set on the base theme, each component holds references to their respective defaults. When setting up the base theme we’ve made opinionated decisions around how these defaults relate to underlying token sets, for instance, we have a system-wide default border. If a user only wants that border to be applied on half their components, they can change this through the defaults. This is an example of a reference theme and hopefully demonstrates the level of flexibility baked into NewsKit.
A currently unrealised goal of this is to generate these defaults as part of our Figma export process, allowing developers to simply drop a generated theme into their website to achieve that brand’s look and feel.
we’ve covered 4 types of themes, the definitions of which I’ve borrowed from this excellent article.
- Simple: If we change tokens we can propagate change through the application
- Contextual: Are will render differently depending on where they are placed, think sub-brands and dark mode.
- Reference: We have a level of indirection allow us to change the token the component is referencing and not just the value of the token itself.
- Full-featured: When we have all of the above combined. If it’s not clear by now this is what NewsKit is 😉.
Hopefully, that gives you a good idea of the journey we’ve been on and why we’ve made some of the architectural choices we have. We believe we have a comprehensive base on which to build the numerous components that our diverse set of different brands across News require. Building components in this way comes at a significant upfront cost and if your organisation is embarking on a similar journey you should weigh up what level of theming you actually require.
The journey for us is far from over, here are some of the ways we’re looking to enhance our theming system in the future:
- Media queries on all overrides & defaults. Currently support is a bit patchy as to where you can apply these, it makes sense to open this up further to give consumers more flexibility.
- Component overrides, for example allowing you to bring your own buttons to the audio player.
- Automated theme export from Figma to code.
Additionally, our high-level roadmap can be found on the NewsKit website.