Skip to main content

Styled components

General principles

Depending on the styling technology (scss, styled-components, css-modules...) we are using, actual style implementations may slightly differ, but generally we want to follow the same guidelines (these code examples will use styled-components as the tool of choice):

  • common styles should be placed within the styles folder
  • we should create variables for reusable values such as colors, z-indexes and so on
export const vars = {
colors: {
pureWhite: "#ffffff",
white: "#f2f2f7",
purple: "#6552ff",
/* ... other colors ... */
grey30: "#D1D1D6",
grey20: "#E5E5EA",
grey10: "#F2F2F7",
},
fonts: {
fontSize: "1.6rem",
fontFamily:
'Roobert, Roboto, system, -apple-system, BlinkMacSystemFont, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, Arial, sans-serif',
fontWeight: 500,
lineHeight: 1.25,
},
transitions: {
hover: "all 0.2s ease-in-out 0s",
drawer: "all 0.3s ease-out 0s",
},
zIndex: {
sidebar: 40,
drawer: 50,
envRibbon: 999,
},
};
  • we want to setup base styles which would include resetting default margins and paddings, and setting box-sizing: border-box which makes it so that borders and paddings are taken into account when setting the width and height of a certain HTML element
  • the most important thing here is to setup the size of 1rem to be 10px, which makes it very simple to calculate sizes during development, and also greatly simplifies the creation of responsive styles since it is easy to just reduce the base font size, depending on the screen size
export const base = ts`
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: inherit;
}

html {
/* this defines the size of 1 rem to be 10px (0.625 of 16px, which is the base browser font size, 0.625 * 16 = 10 */
font-size: 62.5%;
}

body {
box-sizing: border-box;

min-height: 100%;
background-color: ${vars.colors.black};
}
`;
  • we want to also create separate base styles for typography (global font-family, font-size, line-height and so on)
export const typography = css<{ theme: Theme }>`
body {
color: ${vars.colors.white};
font-family: ${vars.fonts.fontFamily};
font-size: ${vars.fonts.fontSize};
font-weight: ${vars.fonts.fontWeight};
line-height: ${vars.fonts.lineHeight};
}

input,
select,
textarea,
button,
pre {
/* form elements don't automatically inherit font-family */
font-family: inherit;
}
`;
  • we should create a util file to make working with media-queries easier (media queries should be defined using em units, reasoning behind that can be found here), note that the actual implementation of the helper may differ depending on if we are using sass, styled-components, or some other styling solution:
export const breakpoints = {
// 36em => 36 * 16px = 576px
phone: "36em",
// 48em => 48 * 16px = 768px
tablet_portrait: "48em",
// 56.25em => 48 * 16px = 900px
tablet_landscape: "56.25em",
// 64em => 64 * 16px = 1024px
desktop: "64em",
// 75em => 75 * 16px = 1200px
desktop_big: "75em",
// 90em => 90 * 16px = 1440px
desktop_huge: "90em",
};

/**
* Helper for defining media queries within styled components.
* Usage example:
* ```
* styled.div`
* background-color: blue;
*
* ${responsive.phone} {
* background-color: red;
* }
* `
*```
* is equivalent to:
*
* ```
* styled.div`
* background-color: blue;
*
* @media only screen and (max-width: 36em) {
* background-color: red;
* }
* `
*```
* */
export const responsive = Object.keys(breakpoints).reduce((acc, key) => {
return {
...acc,
[key]: `@media only screen and (max-width: ${breakpoints[key as keyof typeof breakpoints]})`,
};
}, {} as { [key in keyof typeof breakpoints]: string });
  • other than the base styles, we should strive to keep all other styles local to the component they are used within (in a separate, .styles file)

Components folder

We started with a proposal of folder structure in source files of the components folder.

├── Label
│ ├── Label.stories.tsx
│ ├── Label.styles.ts
│ └── Label.tsx
├── Layout
│ ├── Layout.stories.tsx
│ ├── Layout.styles.ts
│ └── Layout.tsx
|── Button
│ ├── Button.stories.tsx
│ ├── Button.styles.ts
│ └── Button.tsx

.tsx file with the same name as the folder is the entry point of the component module and all JSX logic is contained there. .styles.ts folder contains extracted css-in-js strings, variables, styled components. Story book was not the subject of this talk, but if we had one strapped to our project in .stories.tsx file we would write code to present to component in the Storybook project.


Styling

Basic usage of css prop

We can use styling strings and assign them to a variable

const baseStyle = css`
padding: 0;
background-color: transparent;
text-decoration: underline;
font-weight: 500;
`;

Component usage

With the mentioned file structure in the beginning we're going to show an example on how to create custom components in the said file structure.

Let's say we have the following example

// SomeComponent.styles

import styled from "styled-components";

export const Subtitle = styled.p`
margin-bottom: 3rem;
`;

export const Form = styled.form`
display: flex;
flex-direction: column;
gap: 2rem;

> :last-child {
align-self: flex-start;
}
`;

we are using those styled components in the component with the same name

// usage in a component SomeComponent.tsx

import * as Styled from "./SomeComponent.styles";

export default function SomeComponent() {
return (
<Styled.Form onSubmit={handleFormSubmit} noValidate>
<Input value={bundleId} placeholder="input here" onChange={handleChange} />
<Input value={bundleId} placeholder="input there" onChange={handleChange} />
<Button>Save</Button>
</Styled.Form>
);
}

Style overriding

export const FormButton = styled(Button)css`
background-color: green;
`;

If you are exporting already styled component as a React element you need to add className prop to apply the style overriding

export const SubmitButton = ({ className }) => {
return (
<FormButton type="submit" className={className}>
Submit
</FormButton>
);
};

export const CustomSubmitButton = styled(SubmitButton)`
background-color: red;
`;

If you want to override a component that has children with css classes that do not suit your needs you can wrap the component with styled and override the styling of the said component.

Let's say we have a following example of the Material UI components and we want to override their base classes with custom css

import styled from "styled-components";

const customTabStyling = `
justify-content: center;
padding: 50px 0px 100px 0;

.MuiTabs-scroller {
.MuiTabs-flexContainer {
justify-content: center;
button {
outline: none;
}
}
}
`;

if you noticed we are not using the css prop here because it's not needed for it to work. If you're using Typescript you will also get an error that

Type 'SerializedStyles' is not assignable to type 'CSSObject'

Now if we want to use that style we will do it with the following example

export default styled(function CustomTabs({ className }) {
return (
<Tabs className={className} value={value}>
<Tab value="tab-1" />
<Tab value="tab-2" />
<Tab value="tab-3" />
<Tab value="tab-4" />
<Tab value="tab-5" />
</Tabs>
);
})`
${customTabStyling}
`;

As in a previous example we need to remember if we want this to work and apply the custom styling we need to pass the className prop to the wrapped component even if we are not passing anything into that prop in the parent components where we are using the styled component


Svg elements

We are importing svg elements from our design into assets folder and common icons that are being used as components through the project are then imported into icons.tsx file.

.
src
...
├── assets
├── components
...
└── common
└── icons.tsx

An example for importing the icons from .svg files and exporting them as ReactComponents would be

import { ReactComponent as Close } from "assets/images/icons/close.svg";
import { ReactComponent as Check } from "assets/images/icons/check.svg";
import { ReactComponent as ExchangeDollar } from "assets/images/icons/education/exchange-dollar.svg";

export { Close, Check, ExchangeDollar };

With that, it's very easy to import the component into .styles.ts file and override the Svg component.

// SomeFile.styles.ts
import { ExchangeDollar } from "utils/common/icons";

export const Refund = styled(ExchangeDollar)`
height: 3rem;
width: 2.2rem;
cursor: pointer;
`;

If the Svg is the same name as the styled component that we want to export from the file we just add the Svg suffix to the component.

// SomeFile.styles.ts
import { ExchangeDollar as ExchangeDollarSvg } from "utils/common/icons";

export const ExchangeDollar = styled(ExchangeDollarSvg)`
height: 3rem;
width: 2.2rem;
cursor: pointer;
`;

Reusability

As it was mentioned in previous example we can extract styling strings into objects. That gives us the power to map said objects with styles that can be reusable.

const buttonStyling = {
outline: css`
border: 2px solid grey;
background-color: transparent;

&:hover,
&:focus,
&:active {
background-color: transparent;
border-color: red;
}
`,
textual: css`
background-color: transparent;
text-decoration: underline;
font-weight: 500;

&:hover,
&:focus,
&:active {
background-color: blue;
text-decoration: underline;
}
`,
light: css`
background-color: transparent;

&:hover,
&:focus,
&:active {
background-color: green;
}
`,
};

const OutlinedButton = styled(Button)`
${buttonStyling.outline}
`;

Also reusability can be achieved by using styled components as selectors

export const Title = styled.span`
display: block;
font-weight: 500;
font-size: 36px;
line-height: 43px;
`;

export const AccountDetailsContainer = styled.div`
min-width: 400px;

${Title} {
margin-bottom: 26px;
}
`;

// which is used as

<Styled.AccountDetailsContainer>
<Styled.Title>account details</Styled.Title>
<Styled.Content>Some content here</Styled.Content>
</Styled.AccountDetailsContainer>;

Style importing

If we have another .styles.ts from another file that we want to import, we're importing that into the .styles.ts file of the main component. Let's say for example that we have a Label and we want to include some styles from Layout in the following structure.

├── Label
│ ├── Label.stories.tsx
│ ├── Label.styles.ts
│ └── Label.tsx
├── Typography
│ ├── Typography.stories.tsx
│ ├── Typography.styles.ts
│ └── Typography.tsx

We're importing the styles

// Label.styles.ts

import Typography from "../Typography/Typography.styles.ts";

export const StrongLabel = styled(Typography.Strong)`
...
some css here
...
`;

That style is then used in the Label.tsx file.

// Label.tsx
import Styled from "./Label.styles.ts";

export const SubtitleLabel = ({ text, className }) => (
<Styled.StrongLabel className={className}>{text}</Styled.StrongLabel>
);

As we know from previous Style overriding part we need to add className into our props of the component so that component can be reusable and overridden with custom styles if needed.


Styled components with typescript

We can pass variables in our styled components through custom props directly

const Square = styled.div`
width: 20px;
height: 20px;
border: 1px solid black;
background-color: ${({ color }) => color ?? "red"};
`;

// we then use the styled component in a JSX element

function SomeComponent() {
return (
<div>
<Square color={"blue"} />
<Square color={"pink"} />
<Square color={"purple"} />
</div>
);
}

if we use typescript we can type the props of the styled component. With our internal agreement for the props that styled components are receiving we're adding $ sign as a prefix. In example

const Square =
styled.div <
{ $color: string } >
`
width: 20px;
height: 20px;
border: 1px solid black;
background-color: ${({ $color }) => $color ?? "red"};
`;

function SomeComponent() {
return (
<div>
<Square $color={"blue"} onClick={() => {}} />
<Square $color={"pink"} />
<Square $color={"purple"} />
</div>
);
}

In that way we can differentiate styled component props form native, in this case div, props.