Introduction
The library Styled Components is one of many CSS-in-JS solutions for styling Web apps. I have been a big fan of CSS Modules but, having used Styled Components on a recent project, I am a convert. I have found some rough edges so I thought I would post about how I have improved my Styled Components usage.
Theming
A key feature of Styled Components is that it supports theming. It makes sense to base your theme's shape around the System UI Theme Specification. Your theme will be compatible with any Styled Components-based library that uses it. For example, both Styled System and XStyled System follow the specification.
Note: XStyled System has changed significantly since this post was written. It still looks like an excellent library to use in combination with Styled Components.
The specification defines a set of key/value pairs for the theme. Example keys are space
, fontSizes
, and colors
. You need to decide what the type of each value will be. Generally your choice is for a value to be an array, an object, or an array with aliases:
// space as an array:
space = [0, 4, 8, 16, 32, 64];
// space as an object:
space = {
small: 4,
medium: 8,
large: 16,
};
// space as an array with aliases:
space = [0, 4, 8, 16, 32];
space.small = space[1];
space.medium = space[2];
space.large = space[3];
Sometimes the best choice is obvious, such as using an object for colors
. But with theme keys like space
there are arguments for using an array or an object.
TypeScript
I write Web apps in TypeScript when I can. Styled Components comes with TypeScript typings. They declare the theme object as an empty interface called DefaultTheme
. This supports you being able to type that theme object according to the theme object you are using in your app. You can create a d.ts
file in your project and populate it like so, where theme
is your theme object:
import { theme } from "./wherever";
declare module "styled-components" {
type AppTheme = typeof theme;
export interface DefaultTheme extends AppTheme {}
}
This works because TypeScript merges interfaces that have the same identifier in the same namespace.
CSS as a JavaScript object
If you create a simple styled component with only static CSS then the result is compact and readable:
const StyledExample = styled.div`
color: white;
background-color: #783654;
padding: 2rem;
margin: 1rem;
font-family: sans-serif;
font-size: 1.5rem;
`;
When you include references to theme values then the lambdas make the component harder to read:
const StyledExample = styled.div`
color: ${(props) => props.theme.colors.white};
background-color: ${(props) => props.theme.colors.primary900};
padding: ${(props) => props.theme.space[3]};
margin: ${(props) => props.theme.space[4]};
font-family: ${(props) => props.theme.fonts.display};
font-size: ${(props) => props.theme.fontSizes[4]};
`;
I saw a tweet by @siddharthkp (now deleted) that demonstrated how the above can be rewritten:
const StyledExample = styled.div(
({ theme }) => css`
color: ${theme.colors.white};
background-color: ${theme.colors.primary900};
padding: ${theme.space[3]};
margin: ${theme.space[4]};
font-family: ${theme.fonts.display};
font-size: ${theme.fontSizes[4]};
`
);
You might find this version easier to read. The technique is in the Styled Components documentation in the section on writing CSS as JavaScript objects. Note that the css
function is not required in the above example. I use it here to trigger highlighting of the enclosed CSS when using the vscode-styled-components extension.
Helper libraries
If you are writing styled components from scratch then you can also use Styled System or XStyled System. Both have the same benefits:
- Simplifying the creation of Styled Components that expose style props.
- Providing a succinct syntax for responsive styles.
- Providing alternative ways to access theme values.
The XStyled System site explains on this page why two libraries exist for this rather than one.
Styled System and XStyled System have TypeScript declarations in the Definitely Typed repository
Using helper libraries to add style properties
Sometimes you want a styled component to be configurable via props. You might want to create a generalized flexbox-based Box
component. It would support props for various flexbox, background, and spacing CSS properties. You could of course create this yourself:
type Props = {
flexDirection: import("csstype").FlexDirectionProperty;
justifyContent: import("csstype").JustifyContentProperty;
alignItems: import("csstype").AlignItemsProperty;
padding: import("csstype").PaddingProperty<string>;
margin: import("csstype").MarginProperty<string>;
color: import("csstype").ColorProperty;
backgroundColor: import("csstype").BackgroundColorProperty;
boxShadow: import("csstype").BoxShadowProperty;
zIndex: import("csstype").ZIndexProperty;
};
const Box = styled.div<Props>`
min-width: 0;
display: flex;
flex-direction: ${(props) => props.flexDirection};
justify-content: ${(props) => props.justifyContent};
align-items: ${(props) => props.alignItems};
padding: ${(props) => props.padding};
margin: ${(props) => props.margin};
color: ${(props) => props.color};
background-color: ${(props) => props.backgroundColor};
box-shadow: ${(props) => props.boxShadow};
z-index: ${(props) => props.zIndex};
/* probably a bunch of other user-defined rules here */
`;
You can use the component like so:
import React from "react";
import { ThemeContext } from "styled-components";
const SomeComponent = () => {
const theme = React.useContext(ThemeContext);
return (
<Box
flexDirection="column"
backgroundColor={theme.colors.primary900}
padding={theme.space[3]}
/>
);
};
This approach has problems:
- The CSS is verbose.
- You have to manually access theme values and pass them as prop values.
- The props do not support responsive values. What if you wanted the padding to be larger on a desktop display?
To solve these problems, you could instead use XStyled System or Styled System:
import {
backgrounds,
color,
flexboxes,
positioning,
shadows,
space,
BackgroundsProps,
ColorProps,
FlexboxesProps,
PositioningProps,
ShadowsProps,
SpaceProps,
} from "@xstyled/system";
type Props = FlexboxesProps &
SpaceProps &
ColorProps &
BackgroundsProps &
ShadowsProps &
PositioningProps;
const Box = styled.div<Props>`
min-width: 0;
display: flex;
${flexboxes}
${space}
${color}
${backgrounds}
${shadows}
${positioning}
`;
While the names of the functions and props types differ between the two libraries, the effect is the same. The resulting styled component is both easier to write and easier to use:
import React from "react";
const SomeComponent = () => (
<Box
flexDirection="column"
backgroundColor="primary900"
padding={{ xs: 3, lg: 5 }}
/>
);
The only caveat is that there is no strong typing of theme keys, like primary900
in the above example. But the advantages outweigh this disadvantage.
Both libraries include some functions that add multiple properties to your styled component. For example, space
includes multiple margin and padding props. There are also finer grained functions like marginTop
that add only one or a few properties. This allows you to make your styled component as flexible or constrained as it needs to be. For example, I only wanted a single marginTop
prop on the following styled component:
import styled from "styled-components/macro";
import { marginTop, MarginTopProps } from "@xstyled/system";
const Stack = styled.div<MarginTopProps>`
& > * + * {
${marginTop}
}
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 100%;
`;
This also allows me to support responsive marginTop
prop values:
const SomeComponent = () => (
<Stack marginTop={{ xs: 2, lg: 4 }}>
<div>One</div>
<div>Two</div>
</Stack>
);
Custom style properties in helper libraries
In the previous section I created a Stack
component with a single marginTop
prop. I preferred to give that prop a different name, one that better reflects what it represents. For example, I wanted to name the prop verticalSpacing
.
XStyled System exports a style
function for this purpose:
import { MarginTopProps, style, getSpace } from "@xstyled/system";
const verticalSpacing = style({
// Can be an array, to support synonyms, e.g., marginTop and mt:
prop: "verticalSpacing",
// Can be an array, to support setting multiple CSS properties:
cssProperty: "marginTop",
themeGet: getSpace,
});
type Props = { verticalSpacing: MarginTopProps["marginTop"] };
Styled System provides similar functionality via the system
function as described here:
import { system, MarginTopProps } from "styled-system";
const verticalSpacing = system({
verticalSpacing: {
// Can be an array, to support setting multiple CSS properties:
property: "marginTop",
scale: "space",
},
});
export type VerticalSpacingProp = {
verticalSpacing: MarginTopProps["marginTop"];
};
Using helper libraries to access theme values
XStyled System includes getter functions for each type of theme prop. You can use them to rewrite the example styled component from the 'CSS as a JavaScript Object' section:
import { getColor, getSpace, getFont, getFontSize } from "@xstyled/system";
const StyledExample = styled.div`
color: ${getColor("white")};
background-color: ${getColor("primary900")};
padding: ${getSpace(3)};
margin: ${getSpace(4)};
font-family: ${getFont("display")};
font-size: ${getFontSize(4)};
`;
Or you can use the th
function which has the same getter functions attached to it:
import { th } from "@xstyled/system";
const StyledExample = styled.div`
color: ${th.color("white")};
background-color: ${th.color("primary900")};
padding: ${th.space(3)};
margin: ${th.space(4)};
font-family: ${th.font("display")};
font-size: ${th.fontSize(4)};
`;
The getters are a more compact syntax compared to accessing the theme
prop via lambda functions. They have the disadvantage of not supporting strongly typed theme property access. For example, the string "display"
in getFont("display")
may or may not be an actual theme prop alias. There is also no autocomplete in your IDE. But these getter functions process their arguments using the same algorithm as the custom style properties. The only exception is they do not support responsive values.
This can be very useful. Imagine that I want to allow you to specify the margin value around it via a prop. But the final value that the margin
CSS property will have is an adjusted version of that value:
type Props = { margin: string };
const StyledExample = styled.div<Props>`
margin: calc(${(props) => props.margin} / 2);
`;
Because it is an adjusted value, I cannot use the margin
style property:
import { margin } from "@xstyled/system";
const StyledExample = styled.div<Props>`
/* No way to use calc here! */
${margin}
`;
An advantage of the margin
style property is that it allows you to specify the prop value in two ways:
- As a theme space index or alias, like
2
orlarge
. - As a regular CSS value, like
1.5rem
or30px
.
At the moment only the latter style is permitted. But by using a getter I can offer this choice:
import { getSpace } from "@xstyled/system";
type Props = {
spacing: string | number;
};
const StyledExample = styled.div<Props>`
margin: calc(${(props) => getSpace(props.spacing)} / 2);
`;
I use it like so:
// A theme space prop index
const ExampleOne = () => <StyledExample spacing={3}>One</StyledExample>;
// A theme space prop alias
const ExampleTwo = () => <StyledExample spacing="large">Two</StyledExample>;
// Can still set a custom pixel value
const ExampleThree = () => <StyledExample spacing={40}>Three</StyledExample>;
// Can still set a custom rem value
const ExampleFour = () => <StyledExample spacing="10rem">Four</StyledExample>;
@xstyled/styled
offers an extra form of getter. If you use the styled
object from @xstyled/styled-components
, you can write theme values directly into your CSS:
import styled from "@xstyled/styled-components";
const StyledExample = styled.div`
color: white;
background-color: primary900;
padding: 3;
margin: 4;
font-family: display;
font-size: 4;
`;
This makes for compact CSS rules, although you still do not get strong typing. This will definitely be too much magic for some developers.
Styled System includes a single getter function that is available in the @styled-system/theme-get
package. You can use it like so:
import { themeGet } from "@styled-system/theme-get";
const StyledExample = styled.div`
color: ${themeGet("colors.white")};
background-color: ${themeGet("colors.primary900")};
padding: ${themeGet("space.3")};
margin: ${themeGet("space.4")};
font-family: ${themeGet("fonts.display")};
font-size: ${themeGet("fontSizes.4")};
`;
(At the time of writing, @styled-system/theme-get
does not have a TypeScript declaration file.)
I do not think this getter is as useful as the ones in @xstyled/styled
. The user would have to pass theme prop paths to access theme prop values:
const SomeComponent = () => (
<StyledExample spacing="space.4">Hello you</StyledExample>
);
Mixins
Mixins are straightforward to create. This is a mixin that visually hides an element while still leaving it visible to screen readers:
const visuallyHidden = () => css`
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
`;
Note that the css
function is not required in the above example. It is being used to trigger highlighting of the enclosed CSS when using the vscode-styled-components extension.
You can use this visuallyHidden
mixin like so:
const SomeComponent = styled.span`
${visuallyHidden}/* other CSS rules here */
`;
The mixin will automatically get the styled component's props passed to it. This means you can create a mixin that accesses those props:
type Props = { someProp: string };
const someMixin = (props: Props) => css`
overflow: ${(props) => props.someProp};
`;
This is used in the same way as the previous mixin:
type Props = { someProp: string };
const SomeComponent = styled.span<Props>`
${someMixin}/* other CSS rules here */
`;
Conclusion
Styled Components is a popular library for CSS-in-JS and it has a rich ecosystem. Hopefully I have expanded your knowledge of the ecosystem and demonstrated some patterns for you to adopt.
Changelog
- 2019-11-10 Initial version
- 2020-06-28 Minor formatting and grammatical changes
- 2020-08-27 Plain English improvements
- 2021-10-21 Added a note about the new version of xstyled
# Comments
Comments on this site are implemented using GitHub Issues. To add your comment, please add it to this GitHub Issue. It will then appear below.