Styled-Components is a CSS-in-JS Library designed for the modern web

CSS was initially created for styling documents, which were the primary content of the early internet. However, the emergence of preprocessors like Sass and Less highlights the community’s need for more advanced features than traditional CSS provides. As web applications have become increasingly complex, CSS limitations have become more apparent and challenging to overcome.

Styled-components addresses these limitations by harnessing the power and scoping capabilities of JavaScript, a full-fledged programming language. This approach enables developers to structure their code into components, which simplifies the process of writing and maintaining CSS for large-scale projects. By using styled-components, developers can define the style of a component without worrying about unintended side effects.

Challenges with CSS

While CSS offers the advantage of completely separating style from code, allowing developers and designers to work independently, styled-components can inadvertently lead to tight coupling between style and logic. Max Stoiber, the creator of styled-components, explains how to prevent this issue. Although the concept of separating logic and presentation is well-established, developers might be inclined to take shortcuts when building React components. For instance, it’s tempting to create a single component for a validation button that handles both the click event and the button’s styling. Splitting this functionality into two components requires a bit more effort.

The Container/Presentational Architecture

This architecture promotes a clear separation of concerns: components are responsible for either defining the visual appearance or managing data and logic. Presentation components, in particular, should have no external dependencies; they receive data through props and render the corresponding DOM elements or child components. On the other hand, container components interact with the data layer (e.g., state management libraries like Redux or Flux) but should not be involved in rendering. This Dan Abramov’s article provides a comprehensive explanation of this architecture.

Applying SMACSS Principles

Although the Scalable and Modular Architecture for CSS is specifically designed for organizing CSS, its fundamental concept aligns well with styled-components. SMACSS advocates for categorizing CSS rules into five categories:

  • Base: Contains global styles and resets.
  • Layout: Defines the overall structure of the page and its sections (e.g., header, footer, sidebar, content).
  • Module: Encapsulates styles for reusable UI components.
  • State: Handles variations in element appearance based on their state (e.g., error states, disabled states).
  • Theme: Defines the visual appearance of the application, including colors, fonts, and other customizable elements.

Implementing this separation in styled-components is straightforward. Projects usually start with some form of CSS normalization or reset, which falls under the Base category. Additionally, general font sizes, line heights, and other global styles can be defined either through regular CSS (or a preprocessor) or using the injectGlobal function provided by styled-components.

For Layout rules, if you’re using a UI framework, it likely provides container classes or a grid system that you can leverage. You can easily integrate these classes with your own styles in your layout components.

Styled-components inherently enforces the Module category by associating styles directly with components. Each styled component effectively becomes its own module, allowing you to write styles without worrying about conflicts.

State-based styles can be implemented using variable rules within your components. Define functions that dynamically generate CSS property values based on the component’s state. UI frameworks often provide utility classes for common states that you can apply to your components as well. CSS pseudo-selectors (e.g., hover, focus) can also be used to handle state changes.

Finally, the Theme can be easily incorporated using interpolation within your components. It’s a good practice to define your theme as a set of variables accessible throughout the application. You can even generate colors programmatically (using libraries or custom logic) to handle contrast and highlight colors effectively. Remember that you have the full power of JavaScript at your disposal!

Combining Approaches for Optimal Results

It’s essential to maintain a logical organization for easier navigation. Instead of separating components by type (presentation vs. logic), it’s more effective to group them based on functionality.

For example, you could have a folder for all generic components like buttons. Other components should be organized based on the project’s features. If you have user management functionality, group all related components together.

To effectively combine styled-components’ container/presentation architecture with the SMACSS approach, introduce an additional component type: structural components. This results in three types of components: styled, structural, and container. Since styled-components decorates existing HTML tags or components, structural components are necessary to define the underlying DOM structure. In certain cases, container components can handle the structure of their children. However, when the DOM structure becomes complex or is crucial for visual presentation, it’s best to use separate structural components. Tables, with their potentially verbose DOM structure, are a good example.

Example Project: Recipe App

Let’s build a simple recipe application to illustrate these principles. We’ll start with a Recipes component. Its parent component will act as a controller, managing the list of recipes in the application state and fetching data from an API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Recipes extends Component{
  constructor (props) {
    super(props);
    this.state = {
      recipes: []
    };
  }

  componentDidMount () {
    this.loadData()
  }

  loadData () {
    getRecipes().then(recipes => {
      this.setState({recipes})
    })
  }

  render() {
    let {recipes} = this.state

    return (
      <RecipesContainer recipes={recipes} />
    )
  }
}

This component will render the list of recipes, but it doesn’t need to know how the recipes are displayed. For that, we’ll use another component that takes the list of recipes as input and generates the appropriate DOM structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class RecipesContainer extends Component{
  render() {
    let {recipes} = this.props

    return (
      <TilesContainer>
        {recipes.map(recipe => (<Recipe key={recipe.id} {...recipe}/>))}
      </TilesContainer>
    )
  }
}

We want to display the recipes in a tile grid. It makes sense to create a reusable component for the tile layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class TilesContainer extends Component {
  render () {
    let {children} = this.props

    return (
      <Tiles>
        {
          React.Children.map(children, (child, i) => (
            <Tile key={i}>
              {child}
            </Tile>
          ))
        }
      </Tiles>
    )
  }
}

TilesStyles.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export const Tiles = styled.div`
  padding: 20px 10px;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
`

export const Tile = styled.div`
  flex: 1 1 auto;
  ...
  display: flex;

  & > div {
    flex: 1 0 auto;
  }
`

This component is purely presentational. It defines its own styles and wraps its children within another styled DOM element responsible for the appearance of individual tiles. This structure exemplifies a typical generic presentational component.

Next, we need to define the visual representation of a recipe. We’ll use a container component to manage the relatively complex DOM structure and define necessary styles:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class RecipeContainer extends Component {
  onChangeServings (e) {
    let {changeServings} = this.props
    changeServings(e.target.value)
  }

  render () {
    let {title, ingredients, instructions, time, servings} = this.props

    return (
      <Recipe>
        <Title>{title}</Title>
        <div>{time}</div>
        <div>Serving
          <input type="number" min="1" max="1000" value={servings} onChange={this.onChangeServings.bind(this)}/>
        </div>
        <Ingredients>
          {ingredients.map((ingredient, i) => (
            <Ingredient key={i} servings={servings}>
              <span className="name">{ingredient.name}</span>
              <span className="quantity">{ingredient.quantity * servings} {ingredient.unit}</span>
            </Ingredient>
          ))}
        </Ingredients>
        <div>
          {instructions.map((instruction, i) => (<p key={i}>{instruction}</p>))}
        </div>
      </Recipe>
    )
  }
}

The container component handles some DOM generation, but its logic remains minimal. You can define nested styles within styled-components, so you don’t need a separate styled element for every tag that requires styling. In this case, we do this for the name and quantity of each ingredient item. We could further break down the component and create a separate component for ingredients, but for simplicity, we’ll define it as a styled component within the RecipeStyles file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export const Recipe = styled.div`
  background-color: ${theme('colors.background-highlight')};
`;

export const Title = styled.div`
  font-weight: bold;
`

export const Ingredients = styled.ul`
  margin: 5px 0;
`

export const Ingredient = styled.li`
  & .name {
    ...
  }

  & .quantity {
    ...
  }
`

For this example, we’ll utilize the ThemeProvider from styled-components to inject the theme into the props of styled components. You can access theme variables directly (e.g., color: ${props => props.theme.core_color}), but using a wrapper provides a safeguard against missing theme attributes:

1
const theme = (key) => (prop) => _.get(prop.theme, key) || console.warn('missing key', key)

Alternatively, you can define your own constants in a separate module and use them instead. For example: color: ${styleConstants.core_color}

Advantages

One of the benefits of using styled-components is its flexibility. You can gradually adopt it into your projects alongside your preferred UI framework. You can style most of the layout using traditional CSS and leverage styled-components for reusable components. This incremental adoption approach also facilitates migrating existing projects component by component.

Potential Drawbacks

Designers and style integrators might need to learn basic JavaScript concepts like variables to utilize them in place of preprocessor syntax.

Additionally, they’ll need to familiarize themselves with the project structure. However, finding the styles for a specific component within its corresponding folder can be argued to be more intuitive than searching through multiple CSS/Sass/Less files.

Finally, designers might need to adjust their tooling to accommodate syntax highlighting, linting, and other features for styled-components. this Atom plugin and this babel plugin are good starting points.

Licensed under CC BY-NC-SA 4.0