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 -
- https://www.useanvil.com/blog/engineering/understanding-the-styled-system/.
- https://daily.dev/blog/building-react-micro-components-with-styled-system
Prerequisite
Lets start from the scratch : -
- We will bootstrap a new react project with typescript.
npx create-react-app projectName --template typescript
- Next we will install the necessary libraries namely styled-components and styled-system.
npm install --save styled-components styled-system
- Next we will install type declarations.
npm install --save-dev @types/styled-components @types/styled-system
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 functionspace
. -
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;
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";
Import the theme object and wrap our App component with the ThemeProvider.
<ThemeProvider theme={defaultTheme}>
<App />
</ThemeProvider>
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";
- Then let us import
layout, color, typography, space
andcompose
from styled-system
import { compose, layout, color, typography, space } from "styled-system";
- Let us now create our component.
export const Box = styled.div`
box-sizing: border-box;
${compose(layout, color, typography, space)}
`;
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}
`;
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>;
}
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";
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)}
`;
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.
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>
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>;
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).
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";
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>;
};
Extend our BoxProps with the BoxOptions -
type BoxProps = LayoutProps &
ColorProps<AppTheme> &
TypographyProps<AppTheme> &
SpaceProps<AppTheme> &
BoxOptions;
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",
};
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>
We can also pass Responsive Values to our variant like so -
<Box appearance={["secondary", "primary"]} color="white" fontSize="md">
Hello there
</Box>
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 ofbackground: theme.colors.success200
we can dobackground: "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 ofpadding
orbg
instead of background orsize
instead ofwidth & 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",
};
- 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)}
`;
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"],
},
});
We can add additional CSS properties as props using a shorthand a good example might be the flex
property like so -
system({
flex: true,
});
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"];
};
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
.
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";
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}
`;
- We first create a type for our props, we will extend
BoxProps
, we have omitted display prop as our component will always havedisplay = flex
and we extend the type with theFlexboxProps
type from styled-system. - Second we create a new component called
BaseFlex
, by composing our box component and passing it theflexbox
utility function. - By composing our
Box
, we extend it meaning ourBaseFlex
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 theFlex
to compose new components that will inherit all props & styling of theFlex
component.
export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
(props, ref) => <BaseFlex ref={ref} {...props} />
);
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;
};
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>
);
}
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)
Loving this series! It would be extra awesome if you could cover how to create input/form controls to go with this!