DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on • Edited on

Working with React, Styled Components and Styled System using TypeScript

Introduction

You can find the deployed version of the design system here. In this post we are going to use styled-system with styled-components using TypeScript. This is not an introductory post the reader should be familiar with React and styled-components. If you want to learn more about styled-system check these posts -

Prerequisite

Lets start from the scratch : -

  • We will bootstrap a new react project with typescript.
npx create-react-app projectName --template typescript
Enter fullscreen mode Exit fullscreen mode
  • Next we will install the necessary libraries namely styled-components and styled-system.
npm install --save styled-components styled-system
Enter fullscreen mode Exit fullscreen mode
  • Next we will install type declarations.
npm install --save-dev @types/styled-components @types/styled-system
Enter fullscreen mode Exit fullscreen mode

Theme Object

I would like you to check out the styled-system api page (https://styled-system.com/api), it lists all the utility props provided by the library along with the theme key that a particular prop uses. We will only use some of the keys for simplicity. So our first task is to setup a theme object and it will consist of the following keys -

  • breakpoints an array for our breakpoints. This key is important it will be used by styled-system for handling responsive styling.
  • fontSizes an object containing all our fontSizes. This key will be used by the fontSize props provided by styled-system utility function typography.
  • space an object containing all our spacing. This key will be used by the margin, padding, marginTop, paddingTop, etc. props provided by styled-system utility function space.
  • colors an object containing all our colors. This key will be used by the color, background props provided by styled-system utility function color.

Theme Setup

Under our src folder create a new folder called theme and create an index.ts. Lets create some basic values for the above theme keys under the index file -

export const defaultTheme = {
  breakpoints: ["450px", "600px", "960px", "1280px", "1920px"],

  fontSizes: {
    xs: "0.75rem",
    sm: "0.875rem",
    md: "1rem",
    lg: "1.125rem",
    xl: "1.25rem",
  },

  space: {
    xxs: "0.6rem",
    xs: "0.8rem",
    sm: "1rem",
    md: "1.2rem",
    lg: "1.5rem",
    xl: "2rem",
  },

  colors: {
    white: "#fff",
    black: "#000",

    primary100: "#C6CAFF",
    primary200: "#5650EC",
    primary500: "#3B35DC",

    success100: "#E6FAE7",
    success200: "#52B45A",
    success500: "#2F9237",

    danger100: "#FFECEC",
    danger200: "#E02F32",
    danger500: "#BB1316",

    warning100: "#FFF5EF",
    warning200: "#F17D39",
    warning500: "#D35E1A",
  },
};

export type AppTheme = typeof defaultTheme;
Enter fullscreen mode Exit fullscreen mode

The above code is self explanatory we created the theme object with the 4 specified theme keys discussed in the previous section. We are exporting the theme object and its type declaration from this file.

Now under index.tsx -

import { ThemeProvider } from "styled-components";
Enter fullscreen mode Exit fullscreen mode

Import the theme object and wrap our App component with the ThemeProvider.

<ThemeProvider theme={defaultTheme}>
  <App />
</ThemeProvider>
Enter fullscreen mode Exit fullscreen mode

Box Component

Under the src folder create components folder and create a Box folder under the components folder. Create an index.tsx file under the Box folder. In this file we will create our first component using styled-component and styled-system utility functions.

  • Imports
import styled from "styled-components";
Enter fullscreen mode Exit fullscreen mode
  • Then let us import layout, color, typography, space and compose from styled-system
import { compose, layout, color, typography, space } from "styled-system";
Enter fullscreen mode Exit fullscreen mode
  • Let us now create our component.
export const Box = styled.div`
  box-sizing: border-box;
  ${compose(layout, color, typography, space)}
`;
Enter fullscreen mode Exit fullscreen mode

There you go its done, we use compose if we are to use multiple utility functions for our component, else we would do

export const Box = styled.div`
  box-sizing: border-box;
  ${color}
`;
Enter fullscreen mode Exit fullscreen mode

Typing the Box Component

Lets import the component in App.tsx and use it as follows -

export function App() {
  return <Box bg="primary100">Hello there</Box>;
}
Enter fullscreen mode Exit fullscreen mode

type-error

But we have type errors well that is because TypeScript does not know this component takes in a bg prop, so let us type the Box Component. We have 4 styled system utilities layout, color, typography and space lets us import the same named props from styled-system.

import {
  compose,
  layout,
  color,
  typography,
  space,
  LayoutProps,
  ColorProps,
  TypographyProps,
  SpaceProps,
} from "styled-system";
Enter fullscreen mode Exit fullscreen mode

Create a new type called BoxProps and use it like so -

type BoxProps = LayoutProps & ColorProps & TypographyProps & SpaceProps;

export const Box = styled.div<BoxProps>`
  box-sizing: border-box;
  ${compose(layout, color, typography, space)}
`;
Enter fullscreen mode Exit fullscreen mode

Head over to App.tsx now the errors are gone. Run the app see our component. It should have a backgroundColor with the value of #C6CAFF, meaning styled prop picked this from our theme which is great. Similarly we can pass other props, lets add some padding, color, fontSize.

autocompletion

Hit CTRL+SPACE and you get auto-completion for your props. Check the list of props available we also hand shorthands like we use bg instead of background we can also use p instead of padding. Here is the complete code in App.tsx -

<Box bg="primary100" padding="md" color="white" fontSize="lg">
  Hello there
</Box>
Enter fullscreen mode Exit fullscreen mode

Typing the Theme Values

One thing you might have noticed we don't get auto-completion for the values of styled props from our theme keys. No problem we can type our styled props, import AppTheme in the BoxComponent and make the following changes to the BoxProps -

type BoxProps = LayoutProps &
  ColorProps<AppTheme> &
  TypographyProps<AppTheme> &
  SpaceProps<AppTheme>;
Enter fullscreen mode Exit fullscreen mode

The Styled System exposes these Generic types to which we pass our theme type. The ColorProps will pick the type of the color key of our theme object, the space props will pick the space key and so on. For the layout we don't pass our AppTheme type because we don't have a corresponding size key setup in our theme, you can add it if you want more on that (https://styled-system.com/api/#layout).

tokens-autocompletion

Now hit CTRL+SPACE and we get auto-completion for our design tokens. But there are some caveats, like you now cannot pass any other value other than your theme values. So if you want to pass any other value first add it to your theme.

Variants

Use the variant API to apply complex styles to a component based on a single prop. This can be a handy way to support slight stylistic variations in button or typography components (from the docs).

For the sake of simplicity we will built 2 variants for our box namely primary & secondary. For each of these we will change the bg, color, padding, width and height of the Box.

Lets first add the type -

type BoxVariants = "primary" | "secondary";
Enter fullscreen mode Exit fullscreen mode

We will create another type called BoxOptions that will have additional options that we can pass to our Box as props.

import { variant, ResponsiveValue } from "styled-system";

type BoxOptions = {
  appearance?: ResponsiveValue<BoxVariants>;
};
Enter fullscreen mode Exit fullscreen mode

Extend our BoxProps with the BoxOptions -

type BoxProps = LayoutProps &
  ColorProps<AppTheme> &
  TypographyProps<AppTheme> &
  SpaceProps<AppTheme> &
  BoxOptions;
Enter fullscreen mode Exit fullscreen mode

So our variant prop will be called appearance and it will be of type BoxVariants, notice the ResponsiveValue generic type this enables us to pass arrays for responsive values (https://styled-system.com/responsive-styles). Now lets build our variants -

export const Box = styled.div<BoxProps>`
  box-sizing: border-box;

  ${({ theme }: { theme: AppTheme }) =>
    variant({
      prop: "appearance",
      variants: {
        primary: {
          background: theme.colors.primary100,
          padding: theme.space.md,
          width: "200px",
          height: "200px",
        },
        secondary: {
          background: theme.colors.success200,
          padding: theme.space.lg,
          width: "300px",
          height: "300px",
        },
      },
    })}

  ${compose(layout, color, typography, space)}
`;

Box.defaultProps = {
  appearance: "primary",
};
Enter fullscreen mode Exit fullscreen mode

To the variant function we pass the prop that our component will take and our variants object, each key of which will be a variant. Also note that compose is called after variant, this is done so that our styled props can override the variant styles. Say we had this rare case where we want primary variant styles to apply on our Box but we want to override the padding to say 'sm' in that case we can do so. Had our variant function was called before compose we would not be able to override its style . Now under App.tsx play around with the variants -

<Box appearance="secondary" color="white" fontSize="md">
  Hello there
</Box>
Enter fullscreen mode Exit fullscreen mode

We can also pass Responsive Values to our variant like so -

<Box appearance={["secondary", "primary"]} color="white" fontSize="md">
  Hello there
</Box>
Enter fullscreen mode Exit fullscreen mode
  • But we can go one step further we can directly use our design tokens from our theme in the variants as values to our styles. Like instead of background: theme.colors.success200 we can do background: "success200" styled-system will pick the right token (success200) from the right theme key (colors).

  • We can also use our system shorthands like p instead of padding or bg instead of background or size instead of width & height like so : -

export const Box = styled.div<BoxProps>`
  box-sizing: border-box;

  ${variant({
      prop: "appearance",
      variants: {
        primary: {
          bg: "primary100",
          p: "md",
          size: "200px"
        },
        secondary: {
          bg: "success200",
          p: "lg",
          size: "300px"
        },
      },
    })}

  ${compose(layout, color, typography, space)}
`;

Box.defaultProps = {
  appearance: "primary",
};
Enter fullscreen mode Exit fullscreen mode
  • Difference between the two is that for the latter you don't get type-safety, but given the fact many designers hand over designs with design tokens, it is not a big issue. I would say use whatever you find fit. I will use the first approach for the sake of this tutorial.

System

This one is really interesting. To extend Styled System for other CSS properties that aren't included in the library, use the system utility to create your own style functions. (https://styled-system.com/custom-props).

Let us extend our BoxComponent's styled system props to also accept margin-inline-start and margin-inline-end properties and we will shorten the name to marginStart and marginEnd as follows -

export const Box = styled.div<BoxProps>`
  box-sizing: border-box;

  ${({ theme }: { theme: AppTheme }) =>
    variant({
      prop: "appearance",
      variants: {
        primary: {
          background: theme.colors.primary100,
          padding: theme.space.md,
          width: "200px",
          height: "200px",
        },
        secondary: {
          background: theme.colors.success200,
          padding: theme.space.lg,
          width: "300px",
          height: "300px",
        },
      },
    })}

  ${system({
    marginStart: {
      property: "marginInlineStart",
      scale: "space",
    },
    marginEnd: {
      property: "marginInlineEnd",
      scale: "space",
    },
  })}

  ${compose(layout, color, typography, space)}
`;
Enter fullscreen mode Exit fullscreen mode

We call the system function and pass it an object, whose keys are our property names we intend to extend the system with and the value of these keys is another object to which we pass in

  • property the actual CSS Property we intend to add,
  • and the scale which is the theme key that the system should look into to find the respective token.

So to scale we passed space if we use marginStart prop like marginStart="md" styled system will look into the space key of theme object to find the md value. There are also other options you can read about them in the docs.

We can also target multiple properties using system, say we want maxSize prop which takes in a value and sets the maxWidth and maxHeight for our box we can -

system({
  maxSize: {
    properties: ["maxHeight", "maxWidth"],
  },
});
Enter fullscreen mode Exit fullscreen mode

We can add additional CSS properties as props using a shorthand a good example might be the flex property like so -

system({
  flex: true,
});
Enter fullscreen mode Exit fullscreen mode

This is pretty handy as we don't want to pick any values from the theme and also if the name matches our CSS property we can just pass true as the value for the prop key.

System is a core function of the library many other utility functions like the color we imported are nothing but made from system() - https://github.com/styled-system/styled-system/blob/master/packages/color/src/index.js

Now lets add the marginStart and marginEnd to our BoxProps type, we will do the following -

type BoxOptions = {
  appearance?: ResponsiveValue<BoxVariants>;
  marginStart?: SpaceProps<AppTheme>["marginLeft"];
  marginEnd?: SpaceProps<AppTheme>["marginLeft"];
};
Enter fullscreen mode Exit fullscreen mode

I used the marginLeft field from the SpaceProps because it has the same type and just passed the AppTheme to the SpaceProps so that my values for the prop are typed. Also you can pass responsive values to these props.

autocompletion-system

Hit CTRL+SPACE to see the autoCompletion for our custom system prop.

Composing Components

One Common pattern working with styled-components is composition. Let us now create a new component called Flex which will extend our Box component. To Flex we will pass the flexbox utility function which then enables us to pass props like justifyContent, alignItems other flexbox props.

First create a new folder under components called Flex and create an index.tsx file. Lets import some stuff -

import * as React from "react";
import styled from "styled-components";
import { flexbox, FlexboxProps } from "styled-system";

import { Box, BoxProps } from "../Box";
Enter fullscreen mode Exit fullscreen mode

Make sure that you have exported BoxProps. Let us now compose the Box component and create a new component called BaseFlex and pass the flexbox utility function to it. Also create a type for it.

type FlexProps = Omit<BoxProps, "display"> & FlexboxProps;

const BaseFlex = styled(Box)<FlexProps>`
  display: flex;
  ${flexbox}
`;
Enter fullscreen mode Exit fullscreen mode
  • We first create a type for our props, we will extend BoxProps, we have omitted display prop as our component will always have display = flex and we extend the type with the FlexboxProps type from styled-system.
  • Second we create a new component called BaseFlex, by composing our box component and passing it the flexbox utility function.
  • By composing our Box, we extend it meaning our BaseFlex also takes in all props that we pass to Box, inherits the variants and the system extensions we had for our box (marginStart & marginEnd).
  • In many design-systems we have a base Box component and other components tend to extend it, mainly the Layout components like Flex, Stack, Grid, etc.
  • We can also add variants and extend the system styles for just our Flex and in future can also extend the Flex to compose new components that will inherit all props & styling of the Flex component.
export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
  (props, ref) => <BaseFlex ref={ref} {...props} />
);
Enter fullscreen mode Exit fullscreen mode

Last step we create our actual Flex Component, using React.forwardRef. I actually follow this pattern if you are to pass a ref to a component that is composed and also if you were to have additional custom props or manipulate incoming props before passing them to the styled() function.

There are some TypeScript errors in the Flex component. Some minor type changes are needed to the BoxProps -

export type BoxProps = BoxOptions &
  LayoutProps &
  ColorProps<AppTheme> &
  TypographyProps<AppTheme> &
  SpaceProps<AppTheme> &
  React.ComponentPropsWithoutRef<"div"> & {
    as?: React.ElementType;
  };
Enter fullscreen mode Exit fullscreen mode

We extend the BoxProps with all additional props a div might accept using React.ComponentPropsWithoutRef<"div"> this will solve the TypeErrors in the Flex. Also we will type the polymorphic as prop that we get when we use styled().

Check the Flex Component in App.tsx -

export function App() {
  return (
    <Flex justifyContent="space-between" color="white" size="auto">
      <Box bg="danger500" size="140px">
        Hello One
      </Box>
      <Box bg="success500" size="140px">
        Hello Two
      </Box>
    </Flex>
  );
}
Enter fullscreen mode Exit fullscreen mode

Play with the props and autocompletion, also notice I passed size=auto this is because our Flex inherits Box and our Box has a default variant of primary with only width and height of 200 so we just overwrite its dimensions. size prop comes with the layout utility function of styled-system it is used to set height and width.


And there you go this was a very basic introduction to styled-system using it with TypeScript. This is my first post, your valuable feedback and comments will be highly appreciated.

Also I have used styled-components, styled-system with TypeScript to create a small clone of the awesome Chakra UI Elements I am planning to write a series of posts explaining how I did it. In the meantime do check out my github repo here - https://github.com/yaldram/chakra-ui-clone.

Thanks a lot for reading, until next time, PEACE.

Top comments (1)

Collapse
 
cburrows87 profile image
Chris Burrows

Loving this series! It would be extra awesome if you could cover how to create input/form controls to go with this!