Alex Kondov

Design Systems #3 - Creating a Lean Design System with React

March 11, 2019

In the previous two article in the blog I wrote about the importance of design systems and how implementing a lean one can have a massive productive impact on our work even for small hobby projects.

We went through the philosophical questions and the foundations that we need to have in place. In this article we will go through the setup that I am using for my projects at the time of this writing.

CSS-in-JS

CSS-in-JS is an approach in which the styles of a particular component are coupled to its implementation. Much like how we are using JSX to implement both the markup and the logic, there is a trend to put the style rules that apply to this component together with them. The library I'm using is Styled Components.

const Button = styled.div`
  background: black;
  padding: 10px 20px;
  font-size: 1.2rem;
`

There is a big and possibly still ongoing debate (if you are reading this from the future) about whether CSS should be decoupled from UI components or not.

This goes to one of the basic principles of programming - the separation of concerns. Developers who are in favor in splitting styles and implementation believe that they serve different purposes and should therefore be put in different places.

Others who see components as the building block of the UI say that since the particular styles, logic and markup all relate to a single UI component they should be put together.

Components as Separation of Concerns

I am in favor of the opinion that code that serves to build a particular component should be grouped together. I've been using Styled Components for some time now and find that building UIs this way is closer to my understanding of what a component should be.

For me each component defines its own scope when in comes to separation of concerns. Each building block should contain everything it needs to function properly. I don't see markup, styles and logic as different tools but as complementary ones that serve the same purpose.

Styled Components and Themes

Styled Components provide us with a powerful API that allows us to set styles which can be accessed by all components in a particular scope. We have access to the Theme Provider which uses the Context API to pass down a set of props that we can use.

To take advantage of that API we need to define our theme as a JavaScript object. We will use the styles from the previous article.

const theme = {
  colors: {
    primary: 'hsl(211, 28%, 50%)',
    primaryLight: 'hsl(211, 28%, 80%)',
    primaryDark: 'hsl(211, 28%, 20%)',

    primary: 'hsl(2, 64.5%, 58%)',
    primaryLight: 'hsl(2, 64.5%, 80%)',
    primaryDark: 'hsl(2, 64.5%, 20%)',

    grey: '#65737e',
    greyLight: '#c0c5ce',
    greyDark: '#343d46',

    black: '#1a1a1a',
  },
}

Since we are writing JavaScript we need to have the values as strings. Not getting good highlighting can be a bit of a downside but after all we will be doing in only once. I like to separate the colors, fonts and spacing at the top level for reasons that you will see later in the article. There is no strict schema so you can put them all at one level if you like it better.

const theme = {
  // colors ...
  fonts: {
    serif: '"Merriweather", serif',
    sansSerif: '"Montserrat", sans-serif',
    fontSizeBase: '1rem',
    fontSizeSmall: '0.875rem',
    fontSizeSmallest: '0.75rem',
    fontSizeMedium: '1.5rem',
    fontSizeLarge: '1.875rem',
    fontSizeLargest: '2rem',
  },
  // spacing ...
}

Theme Provider

After we have the styles filled in we need to return our attention to the ThemeProvider again. In order for our components to take advantage of it we need it to wrap the part of the app in which we want to use our theme. For simplicity it's easiest to just wrap the top level of you application with the Provider.

return (
  <ThemeProvider theme={theme}>
    <App />
  </ThemeProvider>
)

It accepts the theme as a props and that's it. From now on all our styled components placed inside <App /> will have access to the theme. Let's see how we can implement the Button component from the beginning using the styles from our lean Design System.

const Button = styled.div`
  background: ${props => props.theme.colors.primary};
  padding: ${props =>
    `${props.theme.spacing.xs} ${props.theme.spacing.sm}`};
  font-size: ${props => `${props.theme.fonts.fontSizeMedium}`};
`

We no longer need to copy and paste color values or sizes across our components or do global search and replace when we want to do a global style change. Even though it's kind of verbose I find the ease of use to be worth it. Only with a few lines we introduced a fully working and highly flexible approach to theming in our application.

Limiting Verbosity

Even if we can stop here, I find writing all those nested paths every time a bit of a bummer. We are trying to save time and we end up with lines that exceed Prettier's default configs when we want to use the padding shorthand and that's not ideal.

There are libraries that can help us with this but since we have been mentioning the word lean over and over again we need to look for an alternative. I believe that we can introduce a couple of helper functions that can limit the verbosity of the theme we currently have while not going beyond the limits of verbosity.

const color = color => props => props.theme.colors[color]

const font = font => props => props.theme.fonts[font]

const spacing = space => props => props.theme.spacing[space]

The example we are looking here uses the colors section of the theme but changing the property will work for the others as well. This function is curried - it takes its parameters one at a time until it receives all that it needs. In this example it is two - the color that we want to use and the props object. The reason that the color is first is so that we can achieve the following API.

const Button = styled.div`
  background: ${color('primary')};
  font-size: ${font('fontSizeMedium')}};
  padding-top: ${spacing('xs')};
  padding-right: ${spacing('sm')};
  padding-bottom: ${spacing('xs')};
  padding-left: ${spacing('sm')};
`

Spacing is a bit trickier because we often use the shorthand operator and use multiple values at once. This can be solved in two ways. Either use the more verbose syntax and set the styles each at a time or implement the ability to use arrays as arguments.

Still something in me tells me that we can optimise the utility functions we have a bit more. We removed a bit of copy/pasting effort but can still think of a smarter way to implement the helper functions in case we decide to add new pieces to the theme in the future. If we use curry our function one level more we can make a more reusable foundation.

const getTheme = themeProp => styleProp => props =>
  props.theme[themeProp][styleProp]

const color = getTheme('colors')

const font = getTheme('fonts')

// And so on...

Adding support for multiple values at once would look something like this.

const getTheme = themeProp => styleProp => props => {
  if (Array.isArray(styleProp)) {
    return styleProp
      .map(prop => props.theme[themeProp][prop])
      .join(' ')
  } else if (typeof styleProp === 'string') {
    return props.theme[themeProp][styleProp]
  }
}

Which will let us use the following API.

const Button = styled.div`
  ... padding: ${spacing(['xs', 'sm'])};
`

If you don't mind the clunkyness of the array syntax this is a fine solution to the problem, but I try to avoid stacks of different brackets whenever I can. If we take advantage of the Array rest operator we can have our function take an indefinite amount of arguments and always treat them like an array. This way we will remove the conditional statements and make the function API even cleaner.

const getTheme = themeProp => ...styleProps => props =>
  styleProps
    .map(prop => props.theme[themeProp][prop])
    .join(' ')

Which means that we can now remove the array brackets and use as many arguments as we need depending on the property.

const Button = styled.div`
  background: ${color('primary')};
  font-size: ${font('fontSizeMedium')}};
  padding: ${spacing('xs', 'sm')};
`

Adding themes

With this in place we can now say that we have successfuly implemented our lean design system and laid the foundations for its scaling in the future. We can add more styles and sections at the cost of a few more getters as utility functions.

We can also painlessly support multiple themes by overriding the sections of the theme object that we want to modify. For example, changing the color scheme of the application would be as easy as creating a new theme object and changing the colors that we need under the appropriate section. Then we can spread the two objects.

<ThemeProvider theme={{ ...baseTheme, ...darkTheme }}>
  ...
</ThemeProvider>

Conclusion

I have been using this approach for my personal projects in the past few months and the benefits of it vastly outweigh the time you will put in implementing it. If you are used to Styled Components or CSS-in-JS libraries, once everything is in place creating a new component is a piece of cake. Putting the styling considerations out of the way allows us to focus on implementing the business logic rather than fishing for colors and aligning elements.

TL;DR

  • When using component based UI libraries we should think about separation of concerns differently
  • Each component should be used as a building block containing everything that it needs when it comes to markup, styles and functionality
  • CSS-in-JS libraries, although controversial are quite powerful if we look at components this way
  • Using a library like this in our React app lets us build a set of styling rules without magic - only objects and functions.

Join My Newsletter

I'm running a small newsletter. Join 300+ others to get my thoughts and musings on software engineering and philosophy every now and then. No news, tutorials, ads or spam.

Alex Kondov

Alex Kondov

Blog of Alex Kondov - I write code during the day. Play Dungeons & Dragons by night. Lift weights in between.