Styling in React with styled-components

styled-components is one of the most commonly used CSS-in-JS libraries for React.

Introduction

The styling of your website or web application is an important part of creating a good user experience. You cannot just ship HTML and JavaScript and expect the app to look decent. When it comes to styling a React app, there are plenty of options to choose from.

  1. the CSS way:
    a. good old fashioned CSS (inline, <style> tag or linked stylesheets)
    b. using modern build tools for the adventurous - CSS Modules

  2. the JavaScript way:
    a. inline styling using the Object-based syntax provided out of the box with React
    b. choose from a plethora (yes, that many) of CSS-in-JS libraries.

The CSS-in-JS scenario has been a controversial one at times but no serious developer can deny that they are worth exploring, that's why we will explore the basic APIs and usage of styled-components (one of the most widely used of the available options) by creating a trivial app that displays a list of "Random People" and some info about them. This is just enough to get you started with styled-components by highlighting some of the common real-world use cases.

We will not be going through every bit of the app in detail but you can view the live version of it on Codesandbox and fork the repo on Github. We will be focusing solely on the implementation related to styled-components throughout the app.

Note - The reader is expected to have prior experience of working with React. You can go through Introduction to React for a quick refresher.

Using styled-components

We will be using Codesandbox for bootstrapping the React app which uses the create-react-app under the hood. Once the setup is done we can get started by installing the npm package for styled-components.

$ npm i styled-components

No extra build steps or configurations are needed for styled-components to work. This makes getting started with the library very easy.

Styling in styled-components follows a syntax identical to CSS. It varies slightly for things like keyframes and media queries. We will explore them in details separately. The CSS like syntax uses ES6 Tagged Template Literals. The style rules we write as such are used to generate a CSS stylesheet with unique class names for each set of rules, and attaches those classes to their respective DOM nodes of the components. The generated stylesheet is injected at the end of the <head> section of the HTML document during runtime by default.

styled

Main.js

import styled from 'styled-components';

const Main = styled.div`
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  align-content: flex-start;
`;

export default Main;

styled is the default export of the styled-components. All the DOM elements are present as methods on the styled object. In the snippet above, we are creating the Main component which acts as the parent container for all the components other than our Header. Here, we use styled.div to create a styled component that renders a <div>. The CSS syntax inside the backticks utilizes common flexbox-based layout code.  This is the simplest way to create a styled component. If you go through the source code in the repo, you will come across multiple usages of this API like styled.img, styled.button, etc.

Loader.styled-as-function.js

/* src/components/Loader.js */

.
.
.

/* line 6 */

const Loader = ({ className }) => (<div className={className}><Loading/></div>)

.
.
.

/* line 41 - 46 */

export default styled(Loader)`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
`;

styled can also be invoked as a function (factory call) itself to wrap an existing React component as a styled component. In the above example, we create the Loader as a simple stateless React component and then call the styled factory function with Loader as an argument to wrap it with the necessary styles. Actually, styled.div is also just an alias for styled('div') so it can be used with HTML tags directly as well.

One important thing to note when using the styled as a factory function to wrap a React component is to manually set the className attribute on the DOM elements to this.props.className or else the style rules will not take effect.

Keyframes, pseudo-elements and pseudo-selectors

Loader.keyframes-and-pseudo-elements.js

/* src/components/Loader.js */


/* line 1 - 39 */

import React from 'react';
import styled, { keyframes } from 'styled-components';

import { getColor } from '../utils/theme'

const Loader = ({ className }) => (<div className={className}><Loading/></div>)

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

const Loading = styled.div`
  color: transparent;
  min-height: 2rem;
  pointer-events: none;
  position: relative;
  
  &::after {
    content: "";
    animation: ${loading} .5s infinite linear;
    border: .1rem solid ${getColor('primary')};
    border-radius: 50%;
    border-right-color: transparent;
    border-top-color: transparent;
    display: block;
    z-index: 1;
    left: 50%;
    position: absolute;
    top: 50%;
    height: 1.6rem;
    width: 1.6rem
    margin-left: -.8rem;
    margin-top: -.8rem;
  }
`;

.
.
.

We are creating a simple animated Loader component using keyframes and pseudo-element. styled-components export a keyframes helper function as a named export that is used to create animation keyframes. It also generates keyframes rules with unique names, just like the class names so that we don't have any global name clashes on the generated stylesheet. The API is similar to that of styled.tagname and the generated keyframes object is then used in the animation property for the style rules, as you can see on line 29 in the gist.

The getColor utility function is just used to extract color values from the theme. We will explore the details of that shortly when we talk about theming in styled-components.

For the pseudo-elements, the syntax is more like what is used in SCSS. & refers to the current selector scope and can also be used for nested rules just like in SCSS. The rules under ::after defines the style for the after pseudo-element of the Loader <div>. Remember, just like in CSS, content property needs to set (even if to an empty string '') for the pseudo-element to be valid.

Similarly, pseudo-selectors like :hover, :active, etc. can also be used in styled-components. In the User component style rules, you can see the transition rules are similar to their CSS equivalents.

index.pseudo-selector.js

/* index.js */

/* line 11 - 31 */

.
.
.

`* {
    border: none;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  html,
  body,
  #root {
    height: 100%;
    width: 100%;
  }
  a,
  a:hover,
  a:active,
  a:visited {
    text-decoration: none;
    color: inherit;
  }`;

.
.
.

User.pseudo-selector.js

/* src/User.js */

.
.
.

/* line 40 - 57 */

export default styled(User)`
  width: 280px;
  margin: 10px;
  padding: 10px;
  border: solid 2px ${getColor('secondary')};
  border-radius: 70px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  transition: all 0.25s ease-in;
  position: relative;
  background: ${getColor('secondary')};
  &:hover {
    transform: scale(1.05);
  }
`;

Using props and the css helper

Interpolated functions can be used inside in the tagged template literals. This function is passed all the props available to the component. styled-components pass on all their props and hence the props can be used to customize or alter style rules dynamically. In this Button component, we use the css helper function to create an absolutely positioned button but only if it has a floating prop. The css helper function allows the generation of valid CSS using template literals within interpolations, meaning it is used to generate CSS using `` when inside ${} wrapped inside another  `` (so meta).

Button.js

/* src/components/Button.js */

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

import { getFontSize, getColor } from '../utils/theme';

const Button = styled.button`
  font-size: ${getFontSize('smFont')};
  color: ${getColor('primary')};
  background: ${getColor('light')};
  border-radius: 5px;
  width: 150px;
  display: flex;
  cursor: pointer;
  justify-content: center;
  align-items: center;
  align-self: center;
  height: 30px;
  border: solid 1px ${getColor('primary')};
  ${props =>
    props.floating &&
    css`
      position: absolute;
      bottom: 10px;
      left: 50%;
      transform: translate(-50%, 0);
    `};
`;

export default Button;

User.js

/* src/components/User.js */

.
.
.

/* line 16 - 30 */

const maleBorder = props =>
  props.gender === 'male' ? `solid 4px ${props.theme.color.primary}` : '0';
const femaleBorder = props =>
  props.gender === 'female' ? `solid 4px ${props.theme.color.primary}` : '0';

const Avatar = styled.img`
  border-radius: 50%;
  width: 60px;
  height: 60px;
  position: relative;
  border-bottom: ${femaleBorder};
  border-left: ${femaleBorder};
  border-right: ${maleBorder};
  border-top: ${maleBorder};
`;

.
.
.

In the Avatar styled-component, props.gender is used to customize the border of the <img> tag. For female users there will be border on the left and bottom side but not on the top and right side and vice versa for the male users. The props.theme.color.primary is the primary color of our theme, the code gist in theming section sheds more light on it.

extending styles

Sometimes, changing styles based on props is not enough and we may need to generate multiple variants of a single component. We can easily do that by calling the extend method on that component.

UserProfile.extend.js

/* src/components/UserProfile.js */

.
.
.

/* line 74 - 83 */

const Value = styled.span`
  font-size: ${getFontSize('tn')};
  color: ${getColor('dark')};
  padding: 5px;
`;

const Label = Value.extend`
  font-weight: bold;
  color: ${getColor('primary')};
`;

.
.
.

Here, we need two <span> elements with similar styles, so we extend Value (an existing styled.span) to create Label which has a bolder font and different font color.

Media queries

Modern sites need to be responsive and for that we need media queries. We will use media queries to adapt our UserProfile and its constituent components for different screen sizes.

UserProfile.js

/* src/components/UserProfile.js */

.
.
.

/* line 44 */

const breakPoint = '600px';

.
.
.

/* line 53 - 65 */

const Details = styled.div`
  width: 350px;
  height: 200px;
  display: row;
  margin: 10px;
  jusify-content: center;
  align-items: flex-start;
  color: black;
  @media (max-width: ${breakPoint}) {
    width: 280px;
  }
`;

.
.
.

/* line 85 - 99 */

export default styled(UserProfile)`
  padding: 10px;
  margin: 40px 0;
  display: flex;
  position: relative;
  background: ${getColor('secondary')};
  border-radius: 5px;
  box-sizing: border-box;
  @media (max-width: ${breakPoint}) {
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }
`;

When the screen width is lower than 600px, we change the flex-direction and other alignments rules of the UserProfile component and lower the width of the Details component from 350px to 280px.

Global styles

Sometimes, we need to add global styles for use cases like negating the default link anchor <a> styles or styling the <body> element. The styled-components library provides a helper function as named export for that too - injectGlobal can be used to add global styles using the now familiar tagged template literal syntax.

index.global-styles.js

/* src/index.js */

.
.
.

/* line 10 - 32 */

injectGlobal`
  * {
    border: none;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  html,
  body,
  #root {
    height: 100%;
    width: 100%;
  }
  a,
  a:hover,
  a:active,
  a:visited {
    text-decoration: none;
    color: inherit;
  }
`;

.
.
.

Theming

The styled-components have theming as a first-class feature because it is very common for web apps to have a uniform theme (like colors, typography, etc.). It exports a ThemeProvider wrapper component that accepts a theme props. It accepts an object holding values for theming purposes, which in our case here are colors and font sizes. It injects this theme props to all the styled components using the context API under the hood. If you need the theme props outside of the context of any styled components, there is also a higher-order component withTheme (just like withRouter in react-router-dom) available for that.

In our theme.js file, we create a theme object with various color and fontSize values. We also create a function invertTheme. The ThemeProvider can also be passed a function for something like contextual theming. The function receives the theme object as a parameter from a parent ThemeProvider i.e. any other ThemeProvider that is higher up the tree. We also have two curried functions to help us extract the color and font size values from the theme props. These two are the functions that have been popping up in the style rules of other components. These are simple functions that accept the props argument and return the desired value extracted from it.

App.js

/* src/App.js */

import styled, { ThemeProvider } from 'styled-components';

.
.
.

import { getUsers } from './utils/apiCalls';
import { invertTheme, getColor } from './utils/theme';

class AppContainer extends Component {
  state = {
    users: [],
    themeInverted: false,
  };

  invertTheme = () => {
    this.setState(state => ({
      themeInverted: !state.themeInverted,
    }));
  };

  .
  .
  .

  render() {
    const { users, themeInverted } = this.state;
    return themeInverted ? (
      <ThemeProvider theme={invertTheme}>
        <AppPresentional
          users={users}
          className={this.props.className}
          invertTheme={this.invertTheme}
        />
      </ThemeProvider>
    ) : (
      <AppPresentional
        users={users}
        className={this.props.className}
        invertTheme={this.invertTheme}
      />
    );
  }
}

const AppPresentional = ({ className, invertTheme, users }) => (
  <div className={className}>
    <Header invertTheme={invertTheme}>
      <p>Random People</p>
      <Button onClick={invertTheme}>Invert Theme</Button>
    </Header>
    
    .
    .
    .
  
   </div>
);

export default styled(AppContainer)`
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  min-height: 100vh;
  background: ${getColor('light')}
  font-family: 'Roboto', sans-serif;
`;

index.js

/* src/index.js */

.
.
.

const rootElement = document.getElementById('root');
ReactDOM.render(
  <ThemeProvider theme={theme}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </ThemeProvider>,
  rootElement,
);

theme.js

/* src/utils/theme.js */

export const theme = {
  color: {
    primary: '#5755d9',
    secondary: '#f1f1fc',
    dark: '#454d5d',
    light: '#f8f9fa',
  },
  fontSize: {
    hg: '32px',
    md: '24px',
    sm: '18px',
    tn: '14px',
  },
};

export const invertTheme = ({
  color: { primary, secondary, dark, light },
  ...rest
}) => ({
  color: {
    primary: secondary,
    secondary: primary,
    dark: light,
    light: dark,
  },
  ...rest,
});

export const getFontSize = size => props => props.theme.fontSize[size];
export const getColor = color => props => props.theme.color[color];

In the index.js file we wrap the App component with ThemeProvider and pass it the theme object. This serves as our default theme. As ThemeProvider components can be nested and be used for contextual theming, we rerender a ThemeProvider wrapping the AppPresentional (the App component is divided into two components, container and presentational) if the this.state.themeInverted is true. This time we pass the invertTheme function to it which receives the default theme object as props from the ThemeProvider higher up in the tree and returns a new object with color values swapped around.

Conclusion

styled-components is one of the best solutions available for styling a React application. We have only discussed the most common of the possible use cases but there is much more to the library which you can explore in their documentation. styled-components have great server-rendering support as well. Now you know how to get going, give it a try.