Styled Components – Perfect Styling of React App

Disclaimer

This article will be useful for beginners and, possibly, oldies. This implementation is purely subjective and you may not like it (see you in the comments). Basic React and TypeScript skills are required to understand the material.

Introduction

Styled Componentsone of the popular solutions for writing code using the method CSS in JS… Flexible, simple and, most importantly, fits perfectly into the architecture of the React application.

CSS in JS – description of styles in JavaScript files.

Advantages:

  1. No more className. The ability to transfer classes does not disappear anywhere, but their use is optional and meaningless, now we can write all the styles inside the styled components, and the classes will be generated automatically.

  2. Simple dynamic styling. You no longer need to write ternary operators and juggle className inside the component, now all these problems are solved by throwing props inside the styled components.

  3. Now this is JS. Since styles are now written in the JavaScript ecosystem, it makes it easier to navigate the project and provides a variety of coding options.

  4. StylisJS under the hood. This preprocessor supports:
    4.1. References to the parent &, which is often used in SCSS.
    4.2. Minification – reducing the size of the source code.
    4.3. Tree Shaking – dead code removal.
    4.4. Vendor prefixes a CSS property prefix to provide support for browsers that have not yet implemented a particular feature on a permanent basis.

Over the past six months, Styled Components has become my favorite, and now I try to use it in every possible project. In this article, I want to highlight my best practices and share my experiences.

Installation

Let’s create an application with CRA with TypeScript

npx create-react-app my-app --template typescript
# or
yarn create react-app my-app --template typescript

TypeScript will be a great helper in writing styled components and give us more control and understanding in the code. All further examples will be described in conjunction with TypeScript.

Install Styled Components and types for it.

npm i styled-components @types/styled-components
# or
yarn add styled-components @types/styled-components

Install the extension vscode-styled-components for highlighting and hints in VSCode.

https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components

Let’s create the following file structure at the root of src. We’ll dwell on each of these files later.

--src/
---styles/
----animations.ts
----components.ts
----global.ts
----theme.ts

The basics

Basic example

Let’s take a look at a simple implementation of Styled Components.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

// Создаем стилизованную компоненту
// Присваиваем ей функцию styled.[название тега]
// Приписываем шаблонную строку и внутри пишем CSS стили
const Container = styled.div`
  background-color: #2b2b2b;
  border-radius: 5px;
`

const Title = styled.h1`
  font-weight: 300;
`
const Text = styled.p`
  font-size: 12px
`

// Используем эти компоненты внутри нашего JSX!
export const SimpleComponent = () => (
  <Container>
    <Title>Styled Component</Title>
    <Text>Some text</Text>
  </Container>
)

Dependencies (properties / props)

To make our styled component dependent on values, we need to pass the required parameters as an attribute.

In the case of TypeScript, you need to define a type to describe additional properties.

styled.[название тега] `styles`

Generic type (generic, generic) allows you to reserve space for the type, which will be replaced with a specific one passed by the user in triangle brackets

// Какая-тоКомпонента.tsx

import styled from "styled-components";

// Компонента <Container/> будет ждать на вход
// атрибут bg с любым строковым значением
const Container = styled.div<{bg: string}>`

  // Чтобы получить доступ к зависимостям, 
  // внутри шаблонных строк воспользуемся строковой интерполяцией `${...}`
  // Где вызовем функцию у которой есть параметр props
  background-color: ${props => props.bg};
`

// Если тип занимает много места, 
// то будет лучше вынести его в отельный интерфейс
interface TitleProps {
  weight: 200 | 300 | 400 | 500 | 600 | 700
}
const Title = styled.h1<TitleProps>`
  // Для лучшей читаемости - деструктурируем props,
  // задаем дефолтное значение если это необходимо
  font-weight: ${({ weight = 400 }) => weight};
`

interface TextProps {
  primary: boolean
}
const Text = styled.p<TextProps>`
  color: ${({ primary }) => primary ? '#424242' : '4b4b4b'};
`

export const SimpleComponentWithProps = () => (
  <Container bg='#fcfcfc'>
    <Title weight={300}>Styled Component</Title>
    <Text primary>Some Text</Text>
  </Container>
)

Attributes

We also have access to the attributes when we create the styled component. The attrs method allows you to transform the props of the styled component before using them.

// Какая-тоКомпонента.tsx

import styled from "styled-components";

interface TextInputProps {
  size: number;
}

const TextInput = styled.input.attrs<TextInputProps>((props) => ({
  // Статичные свойства
  // Мы можем задать им значение
  type: "text",
  onFocus: () => console.log("Focused"),

  // Динамическая зависимость
  // Можем изменить её перд отправкой в стили
  size: (props.size || 4) + "px",
}))<TextInputProps>`
  padding: ${({ size }) => size};
  margin: ${({ size }) => size};
`;

export const SimpleComponentWithAttrs = () => <TextInput size={8} />;

It is best not to use static properties unnecessarily inside a styled component, as these properties are often set in JSX. Just be aware of this feature.

Style inheritance

Styled components can inherit styles from other components, which helps avoid code duplication.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

const Text = styled.div`
  font-size: 12px;
`

// TomatoText наследует те же стили и тег, что и Text
const TomatoText = styled(Text)`
  color: "#FF6347";
`

export const SimpleComponentWithExtending = () => (
  <>
    <Text>Simple Text</Text>
    <TomatoText>Tomato Text</TomatoText>
  </>
)

CSS snippet

An important feature is passing style fragments inside the styled component, this approach simplifies coding and prevents repeating style elements.

// Какая-тоКомпонента.tsx

import styled, { css } from 'styled-components'

// Создаем css фрагмент
const fontStyles = css`
  font-size: 12px;
  line-height: 14px;
  font-weight: 700;
`

const Text1 = styled.h1`
  color: blue;
  // Дополняем стили Text1 фрагментом fontStyles 
  ${fontStyles}
`
const Text2 = styled.p`
  color: blueviolet;
  ${fontStyles}
`

export const SimpleComponent = () => (
  <>
    <Text1>Some Text</Text1>
    <Text2>Another some text</Text2>
  </>
)

Global Styles

As befits any web application, let’s add a main stylesheet to our project. Let’s open global.ts and use the createGlobalStyle function to create a component with global styles.

// global.ts

import { createGlobalStyle } from 'styled-components'

export default createGlobalStyle`
  * {
    ...
  }

  *::before,
  *::after {
    ...
  }

  body {
    ...
  }
`

Next, let’s add it to the application.

// App.tsx

import { Routing } from 'routing'

// Импортируем глобальные стили
import GlobalStyles from 'styles/global'

const App = () => {
  return (
    <>
      <Routing />
      { // Добавляем его как тег }
      <GlobalStyles />
    </>
  )
}

export default App

Theme

How about making the app theme one big source of truth in which we store palette, sizes, media queries, and other component-related properties?

In the theme.ts file, declare a variable with all the necessary properties.

// theme.ts

export const baseTheme = {
  colors: {
    primary: '#7986cb',
    secondary: '#2b2b2b',
    success: '#4caf50',
    danger: '#f44336 ',
    
    bg: '#E5E4E8',
    font: '#19191B',
  },

  media: {
    extraLarge: '(max-width: 1140px)',
    large: '(max-width: 960px)',
    medium: '(max-width: 720px)',
    small: '(max-width: 540px)',
  },

  // in px
  sizes: {
    header: { height: 56 },
    container: { width: 1200 },
    footer: { height: 128 },
    modal: { width: 540 },
  },

  // in ms
  durations: {
    ms300: 300,
  },

  // z-index
  order: {
    header: 50,
    modal: 100,
  },
}

This approach saves us from magic numbers, and makes it possible to apply specific values ​​in styles and components.

Magic numbers in code are one of the personifications of evil and laziness in a programmer. This is an integer constant that appears in the code, the meaning of which is difficult to understand.

From now on, we can simply import this constant in the necessary places and use it.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

import { baseTheme } from 'styles/theme'

const StyledHeader = styled.header`
  background-color: ${baseTheme.colors.secondary};
  height: ${baseTheme.sizes.header.height}px;
  z-index: ${baseTheme.order.header};
`

export const Header = () => <StyledHeader>Title</StyledHeader>

It is possible to pass a theme inside styled components using the ThemeProvider so that we don’t have to constantly import our baseTheme.

// App.tsx

import { ThemeProvider } from 'styled-components'

import { Routing } from 'routing'
import GlobalStyles from 'styles/global'

// Импортируем тему
import { baseTheme } from 'styles/theme'

const App = () => {
  return (
    <ThemeProvider theme={baseTheme}>
      <Routing />
      <GlobalStyles />
    </ThemeProvider>
  )
}

export default App

Now, in any styled component that is inside the provider, we have access to the baseTheme.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

const StyledHeader = styled.header`
  // Получаем значение темы внутри стрелочной функции,
  // где деструктурируем props
  background-color: ${({ theme }) => theme.colors.secondary};
  height: ${({ theme }) => theme.sizes.header.height}px;
  z-index: ${({ theme }) => theme.order.header};
`

export const Header = () => <StyledHeader>Title</StyledHeader>

This implementation has one small problem – the code editor does not provide hints when writing theme properties. To solve it, we need to type the theme and extend the DefaultTheme interface.

At the root of src, create an interfaces directory with a styled.ts file, where we describe each theme property.

// styled.ts

export interface ITheme {
  colors: {
    primary: string
    secondary: string
    success: string
    danger: string
    
    bg: string,
    font: string,
  }

  media: {
    extraLarge: string
    large: string
    medium: string
    small: string
  }

  sizes: {
    header: { height: number }
    container: { width: number }
    footer: { height: number }
    modal: { width: number }
  }

  durations: {
    ms300: number
  }

  order: {
    header: number
    modal: number
  },
}

After that, we create a styled.d.ts file in the src root, where we extend the interface of the standard theme using our type.

// styled.d.ts
import 'styled-components';

import { ITheme } from 'interfaces/styled';

declare module 'styled-components' {
  export interface DefaultTheme extends ITheme {}
}

d.ts files – describe the form of a third-party library and let the TypeScript compiler know how to deal with that third-party code.

Let’s not forget to add the ITheme interface to our baseTheme so that all its values ​​are correct.

// theme.ts

import { ITheme } from 'interfaces/styled'

export const baseTheme: ITheme  = {
  // ...
}

Ready!

If anything, we can take this interface directly from styled-components, if necessary.

import { DefaultTheme } from 'styled-components'

Dynamic theme

If we want to create a dynamic theme, for example, to switch a light theme to a dark one, then we need the already familiar ThemeProvider and any state manager to initialize and control the theme.

First, let’s create enum to define the type of our theme in the interfaces folder.

// styled.ts

export enum ThemeEnum  {
  light = "light",
  dark = "dark"
}

export interface ITheme {
  // ...
}

Enum is a construct consisting of a set of named constants called an enumeration list, defined by primitive types such as number and string.

Let’s add DefualtTheme in the d.ts file.

// styled.d.ts

import 'styled-components';
import { ITheme, ThemeEnum } from 'interfaces/styled';

declare module 'styled-components' {
  export interface DefaultTheme extends ITheme {
    type: ThemeEnum
  }
}

Let’s create a dark and light theme based on baseTheme. The bg and font colors are dynamic and will change when switching themes.

// theme.ts

import { DefaultTheme } from 'styled-components'
import { ITheme, ThemeEnum } from 'interfaces/styled'

// ITheme - используется для статичной темы
const baseTheme: ITheme = {
  // ...
}

// DefaultTheme - используется для динамических тем 
export const lightTheme: DefaultTheme = {
  ...baseTheme,
  type: ThemeEnum.light,

  colors: {
    ...baseTheme.colors,
    bg: '#E5E4E8',
    font: '#19191B',
  },
}

export const darkTheme: DefaultTheme = {
  ...baseTheme,
  type: ThemeEnum.dark,

  colors: {
    ...baseTheme.colors,
    bg: '#19191B',
    font: '#E5E4E8',
  },
}

Below is an example of initializing and switching a theme in MobX.

// ui.ts - одно из хранилищ MobX

import { makeAutoObservable } from 'mobx'
import { DefaultTheme } from 'styled-components'

import { ThemeEnum } from 'interfaces/styled'
import { darkTheme, lightTheme } from 'styles/theme'


export class UIStore {
  theme: DefaultTheme = lightTheme
  
  constructor() {
    makeAutoObservable(this)
  }

  get isLightTheme() {
    return this.theme.type === ThemeEnum.light
  }

  // Переключатель темы
  toggleTheme() {
    this.theme = this.isLightTheme ? darkTheme : lightTheme
  }
}

We transfer the theme from the state manager to the ThemeProvider.

// App.tsx

...

  return (
    <ThemeProvider theme={uiStore.theme}>
      <Routing />
      <GlobalStyles />
    </ThemeProvider>
  )
  
...

After that, we implement the theme switching mechanism, assign dynamic colors in the right places, and if desired, you can add animation smoothness.

Microcomponents

These are simple, reusable, styled one-node components that are common in applications. These can be headers, texts, containers, icons, and others.

Let’s go to the components.ts file and create some similar components.

// components.ts

// Пример вертикального разделителя
interface DividerProps {
  height?: number
  heightMob?: number
}
export const Divider = styled.div<DividerProps>`
  height: ${({ height = 8 }) => height}px;

  // Медиа запрос
  @media ${({ theme }) => theme.media.large} {
    height: ${({ heightMob = 4 }) => heightMob}px;
  }
`

// Пример заголовков разного уровня
interface TitleProps {
  weight?: 200 | 300 | 400 | 500 | 600 | 700
}

export const Title1 = styled.h1<TitleProps>`
  font-size: 24px;
  font-weight: ${({ weight = 700 }) => weight};
`

export const Title2 = styled.h2<TitleProps>`
  font-size: 18px;
  font-weight: ${({ weight = 700 }) => weight};
`

I will give an example of use.

// Какая-тоКомпонента.tsx

import { Divider, Title1, Title2 } from 'styles/components' 

export SimpleComponent = () => (
  <div>
    <Title1>Some title H1</Title1>
    <Divider height={16}/>
    <Title2 weight={200}>Some title H2</Title2>
  </div>
)

Animations

For this, Styled Components have a special function keyframes inside which we pass keyframes. Their writing is completely similar to the one in CSS. All animations can be written to a separate file, since now we can store values ​​in a variable.

// animations.ts

import { keyframes } from 'styled-components'

export const spin = keyframes`
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
`

Let’s apply it to one of the microcomponents.

// components.ts

import styled, { css } from 'styled-components'

// Импортируем компоненту FontAwesomeIcon и её пропсы,
// на базе которой мы расширим интерфейс передаваемых данных
import {
  FontAwesomeIcon,
  FontAwesomeIconProps,
} from '@fortawesome/react-fontawesome'

// Импорт ключевых кадров
import { spin } from './animations'

interface FAIconProps extends FontAwesomeIconProps {
  // Временный атрибут
  $animated?: boolean // не будет передан в FontAwesomeIcon
}
export const FAIcon = styled(FontAwesomeIcon)<FAIconProps>`
  ${({ $animated }) =>
    $animated
      ? css`
          animation: ${spin} 4s infinite linear;
        `
      : css`
          animation: none;
        `}
`

Temporary attribute – denoted by the $ prefix. Prevents further props from being rolled into the styled component.

An example of using the FAIcon microcomponents.

// Какая-тоКомпонента.tsx

import { faCog } from '@fortawesome/free-solid-svg-icons'
import { faReact } from '@fortawesome/free-brands-svg-icons'

import { FAIcon } from 'styles/components'

export const SimpleComponent = () => (
  <>
    <FAIcon icon={faCog} color="#a8324a" $animated/>
    <FAIcon icon={faReact} color="#3265a8"/>
  </>
)

Imports

Placing styled components together with your actual components simplifies your project’s file structure. And to improve the readability of the code, we will transfer the styles after the main code.

// Какая-тоКомпонента.tsx

import styled from 'styled-components'

// Импорт микрокомпоненты
import { Title1 } from 'styles/components'

export const Header = () => (
  <StyledHeader>
    <Title1>Some Title!</Title1>
  </StyledHeader>
)

const StyledHeader = styled.header`
  background-color: ${({ theme }) => theme.colors.secondary};
  padding: 0 16px;
`

If the component has grown to a larger scale, then it would be more correct to place the styles in a separate styles.ts file next to the component.

// Какая-тоКомпонента.tsx

import { faSun } from '@fortawesome/free-regular-svg-icons'

import { StyledHeader, HeaderTitle } from './styles'
import { Title1, SupText, FAIcon } from 'styles/components'
import { Button } from 'components/Button'

export const Header = () => (
  <StyledHeader>
    <HeaderTitle>
      <Title1 weight={200}>Plankton</Title1>
      <SupText>React + Mobx + SC</SupText>
    </HeaderTitle>
    <Button
      variant={Button.variant.ghost}
      color={Button.color.secondary}
      size={Button.size.lg}
    >
      <FAIcon
        color={'#c7a716'}
        icon={faSun}
      />
    </Button>
  </StyledHeader>
)

Another good practice is to import styled components compactly.

// Какая-тоКомпонента.tsx

import { faSun } from '@fortawesome/free-regular-svg-icons'

// Импортируем всё из файла styles.
import * as S from './styles'
import * as C from 'styles/components'
import { Button } from 'components/Button'

export const Header = () => (
  <S.Header>
    <S.HeaderTitle>
      <C.Title1 weight={200}>Plankton</Title1>
      <C.SupText>React + Mobx + SC</SupText>
    </S.HeaderTitle>
    <Button
      variant={Button.variant.ghost}
      color={Button.color.secondary}
      size={Button.size.lg}
    >
      <C.FAIcon
        color={'#c7a716'}
        icon={faSun}
      />
    </Button>
  </S.Header>
)

This approach gives us several advantages:

  1. We don’t litter the code with unnecessary imports.

  2. Logical separation. Each styled component now has its own prefix, making it easier to navigate through the code. In my case:
    Without prefix – common components.
    S – Styles for our actual component.
    C – Microcomponents.

  3. Name collision. I often call the main wrapper StyledSomethingThere, now we can safely write S. Something There… This is due to the repeated name of the parent component and the styled wrapper.

Variations

When creating a component, we often try to interpret it in different ways, keeping the main functionality, be it buttons, text fields, cards, and others.

Let’s take a look at this practice using the example of a button, which can be of different sizes.

// Где-тоВКакой-тоКомпоненте.tsx

import { Button } from 'components/Button'

...

<Button size={Button.size.lg}>
  Text
</Button>

...
// Button.tsx

import { PropsWithChildren } from 'react'

import * as S from './styles'

// Размеры кнопок
export enum ButtonSize {
  xs="xs",
  sm = 'sm',
  md = 'md',
  lg = 'lg',
}

// Интерфейс входящих зависимостей
export interface ButtonProps {
  size?: ButtonSize
}

const ButtonComponent = ({
  children,

  // Так как size опциональный, зададим ему значение по умолчанию
  size = ButtonSize.md,
}: PropsWithChildren<ButtonProps>) => {
  return (
    <S.Button size={size}>
      <span>{children}</span>
    </S.Button>
  )
}

// Присваиваем Enum ButtonSize компоненте 
ButtonComponent.size = ButtonSize

export const Button = ButtonComponent

I will not focus too much on the implementation of the component, since that is a different topic. It should be understood here that the size value can only be set within the enum ButtonSize.

Let’s move on to the styles.

// styles.tsx

import styled, { css } from 'styled-components'

// Импортируем enum
import { ButtonSize } from '.'

interface ButtonProps {
  size: ButtonSize
}

export const Button = styled.button<ButtonProps>`
  // ...
  
  ${({ size }) => 
    size === ButtonSize.lg
      ? css`
          height:48px;
          font-size: 18px;
      `
      : size === ButtonSize.md
      ? css`
          height: 40px;
          font-size: 16px;
      `
      : size === ButtonSize.sm
      ? css`
          height: 32px;
          font-size: 14px;
      `
      : css`
          height: 24px;
          font-size: 12px;
      `
  }
`

I think you will agree that this is not the most beautiful solution. Therefore, I propose this alternative: get rid of the ternary operators and create a sizes object from which we will simply take the required value by key.

We get the following result.

// styles.tsx

import styled, { css } from 'styled-components'

import { ButtonSize } from '.'
import { StyledVariants } from 'interfaces/styled'

interface ButtonProps {
  size: ButtonSize
}

export const Button = styled.button<ButtonProps>`
  // ...
  
  ${({ size }) => sizes[size]}
`

const sizes: StyledVariants<ButtonSize> = {
  xs: css`
    height: 24px;
    font-size: 12px;
  `,
  sm: css`
    height: 32px;
    font-size: 14px;
  `,
  md: css`
    height: 40px;
    font-size: 16px;
  `
  lg: css`
    height: 48px;
    font-size: 18px;
  `
}

You must type this object to be sure of the properties you set. Let’s add our interface file.

// styled.ts

// тип css фрагмента
import { FlattenSimpleInterpolation } from 'styled-components'

// E - элемент enum
export type StyledVariants<E extends string | number> = {
  [key in E]?: FlattenSimpleInterpolation
}

What if the variation does not depend on one value, but on two or more? I will add two more dependencies to the button component, variant and color.

// Button.tsx

...

export enum ButtonVariant {
  solid = 'solid',
  outline="outline",
  ghost="ghost",
}

export enum ButtonColor {
  primary = 'primary',
  secondary = 'secondary',
  success="success",
  danger="danger",
}

...

To work with several parameters, we need to create a switch case construction that will return one of the CSS fragments of a certain variation and color.

// styles.tsx

import styled, { css } from 'styled-components'

import { ButtonVariant, ButtonColor } from '.'

interface ButtonProps {
  variant: ButtonVariant
  color: ButtonColor
}

export const Button = styled.button<ButtonProps>`
  // ...
  
    ${({ 
      variant = ButtonVariant.solid,
      color = ButtonColor.primary,
      theme
    }) => {
    const themeColor = theme.colors[color]

    switch (variant) {
      case ButtonVariant.solid:
        return css`
          background-color: ${themeColor};
        `
      case ButtonVariant.outline:
        return css`
          background-color: transparent;
          color: ${themeColor};
          border: 1px solid ${themeColor};
        `
      case ButtonVariant.ghost:
        return css`
          background-color: transparent;
          color: ${themeColor};
          border: none;
          box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px;
        `
    }
  }}
`

Bonus

polished

Nice addon for Styled Components that you should know about. This package provides a lot of new features, such as darkening and lightening colors, converting hex to rgb, making an element transparent with binding to a specific color, and much more.

Let’s leave this library for the sequel.

https://www.npmjs.com/package/polished

Colored brackets

While using Styled Components, I ran into one nasty bug in the Bracket Pair Colorizer plugin. Since we write styles inside template strings, brackets of different levels are often incorrectly highlighted. Fortunately, there is a solution, and it is very fresh (at the moment), with the recent update, VSCode introduced its own colored brackets, and as the developers themselves write, their implementation is 10,000 times faster than the plugin.

You just need to add the following parameter to your editor settings:

"editor.bracketPairColorization.enabled": true

https://code.visualstudio.com/blogs/2021/09/29/bracket-pair-colorization

getTransitions

I want to share my helper – this function will simplify the writing of transitions, especially in those places where we want to implement a change of theme.

import { css } from 'styled-components'

export const getTransition = (
  duration: number,
  property: string[] | string = ['background-color', 'color'],
  animation = 'ease'
) =>
  css`
    transition-property: ${Array.isArray(property)
      ? property.join(', ')
      : property};
    transition-duration: ${duration}ms;
    transition-timing-function: ${animation};
  `

An example of use in a styled component.

const Something = styled.div`
  ${({ theme }) =>
    getTransition(theme.durations.ms300, [
      'background-color',
      'border-color',
      'color',
   ])}
`

More examples

In this repositories collected ready-made components, articles, videos, projects created on the basis of Styled Components, and much, much more. I advise you to look.

https://github.com/styled-components/awesome-styled-components#components

Conclusion

I hope I was able to convey the uniqueness and flexibility of writing CSS code using Styled Components. In my opinion, this is the perfect tool for React projects, where we can afford innovation, unique approaches and a lot of variability!

If you want to look at these practices in practice, then here are the source codes and demo of the project.

Code
Demo client
Demo storybook

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *