A Themeable Design System
I've been involved with design systems for a good number of years. Even before design systems, there where styleguides. Remember those?
Design Systems, Styleguides, pattern libraries, etc. They are all great tools/systems and having created and consumed a variety of these systems over the years I've recently come up against a problem where the "traditional" systems where not flexible enough.
If you go look at any of the big tech companies desgin system(s) they have published you'll notice they are all aimed at the companies usage, which is great. If you are building an integration on top of shopify using their design system polaris to maintain consistency from your integration to shopify's application is great.
Even some big names provide "themes" on top of their systems. If the company has a few identities you are able to use the correct theme for the relevant company.
These are all great, but -
What if you wanted to provide as much flexibility as possible?
What if you didn't know who was going to using the system?
What if you wanted the ability for the system to be flexible enough to apply a companies own style to it?
These are just a few of the questions I was faced with answering recently. How can we provide a core set of components, that are ultimately fully customisable at the core? But also provide an "out-of-the-box" theme.
Based on these questions and more, I came up with following solution which I am going to walk you through.
Technology
For the solution as it is to be used in part of an exisiting ecosystem. The technology stack wasn't completely open. React was a hard requirment, the styling was a bit more open - it was either going to be a CSS-in-JS approach or CSS. I opted for CSS, but with Sass as the integration layer, and the end result being a standalone CSS file.
System Overview
The system is broken down to three parts
- Theme
- Components
- Blocks
The core of the system is a set of components, think of these as the core. The components would be used as the building elements of blocks, example of such components -
- Paragraph
- Image
- Caption
- Date
- Link
These components are very presentational, and are a means to provide a consistent interface to commonly used components of the system. The components are not limited to outputting text, images, etc, a component can also be a layout component.
Blocks are used within the system to provide an entry point and exit point. The entry point being the internal system, and the exit point being the published page. A block is a composed component of functionality that is made up primarily of components. An example of a block would be
- Promo Item
- List of articles
- Search Results list
Theme the theme is what brings it all together and holds the styling information. The theme is a collection of Sass maps that hold the CSS properties and values for a component, a block and also the ability for component within a block to be styled.
System Breakdown
Components
To understand how it all fits together and what is involved at each level, I am going to run through a basic example.
The paragraph component looks as follows
import React from "react";
import PropTypes from "prop-types";
const Paragraph = ({ children }) => <p className="c-paragraph">{children}</p>;
Paragraph.propTypes = {
children: PropTypes.string.isRequired,
};
export default Paragraph;
Very simple and basic React component that takes children and output wraps that in a p
tag, where we apply a class of c-paragraph
.
The class applied to the component is exposed via a Sass file which looks as follows
.c-paragraph {
@include component-properties("paragraph");
}
This would the bare minimum for a component Sass file. It looks a little bare, and not doing anything at the moment. This is because the styling will come from the theme. We are using a mixin provided by the system that would output the CSS properties and values for a paragraph
Blocks
As mentioned earlier, blocks are composed of components. An very simple block could look like
import React from "react";
import PropTypes from "prop-types";
import { Paragraph } from "components";
const FakeStory = {
description:
"One of the highest paved roads in Europe, this mountain pass makes for a thrilling road trip into the misty mountains above the Adriatic and Ionian seas.",
};
const Promo = () => (
<div className="b-promo">
<Paragraph>{FakeStory.description}</Paragraph>
</div>
);
export default Promo;
As you can see it's very basic. The promo block is only using a single component in this example, but it could use any number of the core components if needed. The block has a wrapping div
with a class b-promo
this is then exposed via a block Sass file to allow styling of the block but also components inside a block.
The Sass file for a block is as follows
.b-promo {
@include block-components("promo");
@include block-properties("promo");
}
Similar to components, we are making use of mixins to load in the promo
block styles, but also load in the styles for components within a block. How the component styles work will be explained below as part of the theme.
Theme
The theme is where the styling is controlled for a component, a block and for components within a block.
The theme is utilising Sass to extend the capabilites of CSS by providing some mixins and better variable management. The output from the Sass files is a CSS file that is utilising CSS Variables. The primary reason to use Sass is the power it provides with being able to doing looping and provide functions (mixins).
Within the theme there are multiple Sass maps, following the design token approach it's broken down into four distinct areas
- Global
- Alias
- Components
- Blocks
Global
Global tokens are sets of tokens that are the core to the system. Color pallettes, font sizing, font weight, spacing, etc.
Each token in the global tokens map is output as a CSS Variable prefixed with --global-
.
$global: (
"gray-50": rgb(218 218 218)
);
is then converted into the following CSS
:root {
--global-gray-50: rgb(218 218 218);
}
Alias
Alias tokens are technically still global tokens, but provide the ability to have a friendly name attached to a given global token. For example, primary-color
would be an alias token, and the value of that token would reference a global token.
Each token in the alias map is output as a CSS variable - these are not prefixed at all.
$alias: (
"primary-color": rgb(100 100 100)
);
is then converted into the following CSS
:root {
--primary-color: rgb(100 100 100);
}
Components
Component and block tokens are a little different to the global and alias set up, as these are nested Sass maps that follow the CSS property, and value set up.
For components the nested Sass map is structured with the first key inside the map being a component name. If you remember from before there is a Sass mix that looks up the component tokens by name. Meaning the component name will need to match that which is set in the components .scss file.
$tokens: (
components: (
"paragraph": ()
)
);
Then inside the paragraph
map we have the ability to use CSS properties and values just as if you were writing CSS, allowing you do something like
'paragraph': (
'font-size': 16px,
'font-weight': bold,
),
If we bring back the code from above of how we write the Sass for the paragraph component we can see how they work together
.c-paragraph {
@include component-properties("paragraph");
}
With the above code and the above example the generated CSS would be
.c-paragraph {
font-size: 16px;
font-weight: bold;
}
Obviously that's not great to use hard coded values as the value items in these maps as you loose the power of the token system. Knowing how the previous global and alias tokens are converted you can reference those as the values
'paragraph': (
'font-size': var(--global-font-size-200),
'font-weight': var(--global-font-weight-700),
),
this would then make use of the CSS variable in the outputting CSS as follows
:root {
--c-paragraph-font-size: var(--global-font-size-200);
--c-paragraph-font-weight: var(--global-font-weight-700);
}
.c-paragraph {
font-size: var(--c-paragraph-font-size);
font-weight: var(--c-paragraph-font-weight);
}
Blocks
The blocks part of the theme works in a similar way to components the @include block-properties('promo');
Sass mixing does pretty much the same as the component-properties
version, the only difference is it's looking for the named item within the blocks
map.
$tokens: (
blocks: (
"promo": ()
)
);
The added ability you have within a block is the ability to change the style of a component differently to that of the base components
styling. As we mentioned before blocks are composed of components, this means by default and using the CSS cascade all components will have the same styling when used within a block. But what if you want to make the paragraph
component within the promo
block to be styled slightly different? Say we wanted to increase the font-size
thats where the Sass mixin @include block-components('promo');
comes in.
Given we have the need to update the paragraph
font size within a promo
block the blocks map has the ability for us to define component styles for a given block.
$tokens: (
blocks: (
'promo: (
'components': (
'paragraph': (
'font-size': 18px,
),
),
),
),
);
To break down the nested map it can be a little easier read from the inside out. What the map is saying is for the paragraph
component inside a block
set the font-size
to 18px
.
How this is translated to CSS is by using the power of CSS variables, as previously mentioned we are outputting to CSS variables at all times meaning that we are able redfined the paragraph font size CSS variable within the promo block CSS.
Reminder of how the block Sass file is
.b-promo {
@include block-components("promo");
@include block-properties("promo");
}
Based on the above example of setting the paragraph
font-size
to 18px
within a promo
block the CSS would be output as
.b-promo {
--c-paragraph-font-size: 18px;
}
Working Examples
Code is available on GitHub - Theme Component Repo
It's all demo'd via storybook which is published - Theme Components Storybook