How to Style a React Application

April 17, 2024 24 minute read

We don’t build web applications the same way we did a decade ago. We don’t think of pages, we think of components. We don’t pass data to the template to render, we manage dynamic state. We have powerful APIs that turn the hardest consistency bugs of the past into trivial logic.

But something that’s largely remained the same is styling.

I can immediately tell the difference between a codebase written in 2014 and one from 2024. But if you showed me the stylesheets, they’d all look like they were put together yesterday.

While this means that CSS is mature enough not to warrant a big mindset shift, our front-end development practices and the size of the products we build have changed a lot. Styling remains an unsolved problem for many teams.

I’ve seen plenty of engineers who have no trouble implementing complex state management struggle when it comes to proper styling and responsive design. Front-end development is unique in the regard that it combines both logical constructs and aesthetics, and CSS is often underestimated as the easier part of the two.

Once the logic and the components are ready and functional, making them look pretty is straightforward, right? Right?

I’ve struggled enough with CSS to know better.

A Quick Aside

This is not just an article full of code examples, it’s a chapter of my upcoming book “The Full-Stack Tao” which I’m writing in public.

Here are all published chapters so far:

Semantic Classes

We’ll put aside functionality for the rest of this chapter and focus entirely on the markup of a component and its CSS. Here’s a simple component that renders an essay which we will make prettier.

function Essay({ title, content, author }) {
  return (
    <article>
      <h1>{title}</h1>
      <p>{content}</p>
      <div>
        <img src={author.image} />
        <div>{author.name}</div>
      </div>
    </article>
  )
}

The HTML spec states that our markup should only contain information related to the content. So we should use what we call a “semantic” class to make our elements selectable with CSS.

function Essay({ title, content, author }) {
  return (
    <article className="essay">
      <h1>{title}</h1>
      <p>{content}</p>
      <div>
        <img src={author.image} />
        <div>{author.name}</div>
      </div>
    </article>
  )
}

Semantic, in the context of a class, means that it should explain the meaning of the content inside the element. In the example above we use essay because that’s what our component is rendering.

Then we can use the class as an anchor to select the different elements inside the component.

.essay {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;
}

.essay h1 {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 15px;
}

.essay p {
  font-size: 16px;
  margin-bottom: 10px;
}

.essay div {
  display: flex;
  align-items: center;
}

.essay div img {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
}

.essay div div {
  font-size: 14px;
  font-style: italic;
}

Writing out the complete selector for every element makes our CSS lengthy, so we should use nested selectors instead. They are adopted by the latest browsers, and we have preprocessors for older ones.

.essay {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;

  h1 {
    font-size: 24px;
    font-weight: bold;
    margin-bottom: 15px;
  }

  p {
    font-size: 16px;
    margin-bottom: 10px;
  }

  div {
    display: flex;
    align-items: center;

    img {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      margin-right: 10px;
    }

    div {
      font-size: 14px;
      font-style: italic;
    }
  }
}

Following the fundamental principle of separation of concerns, our markup should be dealing with the content, the CSS should be dealing with styling, and they should be separated. With this approach, we’ve implemented that idea pretty well.

But let’s look at the component and its CSS side by side. They’re almost identically structured.

Especially when the styles are nested we can see how tightly coupled our CSS is to the HTML. If we need to visualize another piece of content with a div it would inherit styles that may not be relevant to it at all.

So while this by-the-book approach is following the principles, it’s too tightly coupled. We should apply more classes to it to make selecting the elements easier.

function Essay({ title, content, author }) {
  return (
    <article className="essay">
      <h1 className="title">{title}</h1>
      <p className="content">{content}</p>
      <div>
        <img className="author-image" src={author.image} />
        <div className="author-name">{author.name}</div>
      </div>
    </article>
  )
}

But following the idea of semantic classes, these look too generic. The .title and .content classes could be referencing multiple elements inside our application, and the author-related classes can be used in various places as well since they relate to a core product entity.

function Essay({ title, content, author }) {
  return (
    <article className="essay">
      <h1 className="essay-title">{title}</h1>
      <p className="essay-content">{content}</p>
      <div className="essay-author">
        <img className="essay-author-image" src={author.image} />
        <div className="essay-author-name">{author.name}</div>
      </div>
    </article>
  )
}

These are a lot better.

Now they relate to a specific part of an entity, and we no longer need to reference element types in the CSS. At the same time we’re avoiding any potential name clashesh between classes that could mess up our styles.

.essay {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;

  .essay-title {
    font-size: 24px;
    font-weight: bold;
    margin-bottom: 15px;
  }

  .essay-content {
    font-size: 16px;
    margin-bottom: 10px;
  }

  .essay-author {
    display: flex;
    align-items: center;

    .essay-author-image {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      margin-right: 10px;
    }

    .essay-author-name {
      font-size: 14px;
      font-style: italic;
    }
  }
}

Our CSS is still very similar to the markup but we’ve at least reduced its knowledge of the exact component structure to a certain extent, limiting its responsibilities to styling

Creating Similar Components

Most web applications try to have a consistent look and feel, and it’s normal for components to be similar. In fact, very often we will find that components need minimal or no tweaks to be used in other parts of the app if it’s designed well.

Our application needs a component to display a quote that we’ll show while the user is waiting for data to load.

It needs to be wrapped in a box, it should render the quote, an image of its author, and their name. It’s very similar to the Essay component we already have and its styles should be identical as well. So in the spirit of not repeating ourselves, we copy the markup and its classes, and voila! Everything looks good.

function Quote({ content, author }) {
  return (
    <article className="essay">
      <p className="essay-content">{content}</p>
      <div className="essay-author">
        <img className="essay-author-image" src={author.image} />
        <div className="essay-author-name">{author.name}</div>
      </div>
    </article>
  )
}

There’s a problem with this approach, though.

The classes in our new component no longer reflect its content. Following the HTML spec, we know we should be writing semantic classes that give meaning to the markup. But since we want to reuse our CSS our only option would be to rename the classes to something more generic that could cover both use cases.

.text-box {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;

  .text-box-title {
    font-size: 24px;
    font-weight: bold;
    margin-bottom: 15px;
  }

  .text-box-content {
    font-size: 16px;
    margin-bottom: 10px;
  }

  .text-box-author {
    display: flex;
    align-items: center;

    .text-box-author-image {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      margin-right: 10px;
    }

    .text-box-author-name {
      font-size: 14px;
      font-style: italic;
    }
  }
}

We reuse the styles, and the classes still reflect the nature of the content in both cases. Our CSS efforts are starting to pay off.

But with this approach, the CSS is not only coupled to one section of markup. For example, the Quote component doesn’t have a title but there’s a selector applying styles to it because of the Essay component.

The CSS becomes coupled to all of the sections that use it. Handling edge cases becomes too similar to managing a widely used programming abstraction.

A Specific Change

Now, after a few weeks of having our app in production, we decide to make some essays highlighted. Specific essays should have a black background with white text on it to draw attention. We won’t focus on the highlighting criteria or the functionality now, only on the styling.

.highlighted {
  background-color: black;
  color: white;
}

We can name the class highlighted and add it to our components with a simple conditional check.

function Essay({ title, content, author, highlighted }) {
  return (
    <article
      className={`text-box ${highlighted ? 'highlighted' : ''}`}
    >
      ...
    </article>
  )
}

But after a couple more weeks, we decide that we want to change the highlighted essays to be light purple, and the text should again become black.

.highlighted {
  background-color: #d5b8ff;
}

We test our changes, they look good, and we deploy the change.

But while we are carefully making sure that our classes reflect the nature of the content every time we reuse them, we might have a colleague who is not as diligent in their work.

They too know of the importance of removing repetition from the codebase, though, so when they had the task of adding a component with text and black background, they reused our .text-box with the .highlighted class, even though their component’s purpose wasn’t matching ours.

We had made our class too generic, and since it wasn’t reflecting the nature of the content it was styling, people decided to reuse it.

.text-box.text-box-highlited {
  // ...
}

With this change we make it obvious where the class is supposed to be used. So they can make a component of their and implement semantic classes specific to it since it serves a different purpose. This hurt our reusability, but the usecases were too different.

There’s another problem right around the corner, though.

Even More Specific Changes

The next feature we need to develop is that the first letter of our Quote component’s text should be capitalized, similar to the first letter in a book chapter.

.text-box {
  // ...

  .text-box-content {
    // ...
    &::first-letter {
      text-transform: uppercase;
      font-size: 200%;
      font-weight: bold;
    }
  }
}

But this would also capitalize the first letter of the Essay component which we don’t want to do. So we should implement this as an additional class that gets added only in the Quote component.

But wait, what should the default styling of the component be?

Should it have normal first letter capitalization only because the Essay component was the first to use it? My philosophy is that the simplest style should be the base, so I’d leave the capitalization to be added as an additional class, but how should we name it?

Again, going back to the HTML spec, classes should be reflecting the content in the element, so we could name it something like .capitalized-first-letter, but then we’d end up with the same problem we had with .highlighted. Someone might take a specific class for a generic one and use it somewhere it’s not supposed to be used.

We can add a specific class like .quote-content but tying it to a generic class breaks up the separation of concerns.

Duplication

Let’s go back to our previous problem - two components have many styles that repeat between them but there are enough specifics to prevent us from reusing a class.

.author-image {
  display: flex;
  justify-content: center;
  align-items: center;

  border: 1px solid black;
  border-radius: 50%;
}

.company-image {
  display: flex;
  justify-content: center;
  align-items: center;

  border: 1px solid #d5b8ff;
  border-radius: 5px;
}

We could extract a class that would describe just the repetitive behavior since we expect to have a lot of elements that will need to use flexbox and be centered.

??? {
  display: flex;
  justify-content: center;
  align-items: center;
}

.author-image {
  border: 1px solid black;
  border-radius: 50%;
}

.company-image {
  border: 1px solid #d5b8ff;
  border-radius: 5px;
}

But now we’ve increased the level of coupling between our HTML and CSS even more. Our markup will have to be aware of our styling decisions. The separation of concerns is broken up again.

In the past, we reached for SCSS mixins to reuse smaller pieces of logic, but remember that every decision we make is a balance on the scale between over and under-engineering. With the front-end tooling we have available nowadays, that’s unnecessary complexity.

**After tackling similar problems for years, I came to the conclusion that reusable CSS is a little bit of a red herring. **

There are many elements on the screen that are similar, yet different in specific cases. Yes, granular classes about buttons, inputs, and low-level components are reusable, but the more specific something gets, the harder it is to reuse any of your styles.

Insufficient Styling

If an element’s classes are not designed to be reusable you will find that some of its styling may be coming from its parent like spacing, fonts or colors. This means that we can reuse “some” of the CSS, but then we’d have to duplicate the rest in a class of our own.

Hardcoded Values

So I accepted repetition as a necessary evil. Thinking about proper CSS architecture adds a lot of unneeded complexity to a project that we don’t have the right tools to tackle. We’re essentially implementing a form of inheritance but without the smart intellisense that we get when writing code.

So I kept writing out the styles again and again - the margins, the fonts, the colors, and whatnot.

While doing this for the thousandth time, pondering the ideas of separation of concerns, I noticed that I’ve been breaking another important principle, one that we’ve already established in our codebase.

Avoid magical hardcoded values.

Abstract the Style Values

When I looked at my styles they were not only repetitive but also riddled with magical values. Various colors, margins, and every imaginable font size from 10 to 48 pixels held the UI together.

Just like in our codebase, these numbers don’t describe their purpose.

For example, you don’t know how font-size: 24px is related to the current application. How large is the text exactly? In a dashboard, this could be a heading, but in a brutalist landing page, this could be the normal size of the text on the page.

But cognitive load is not our only problem. Consistency and symmetry in the screen are what makes a product look good, and having a good-looking end result is just as important for any front-end application as its state management.

Consistency Through Design Tokens

A UI without consistent spacing feels “off” to the people using it even if they can’t pinpoint the problem.

We want to extract common values not only because we want to reuse them, but also because we want to limit which of them should be used in the first place. Using font sizes, margins, and paddings that fit on a scale gives a sense of symmetry and consistency to the UI.

And the same goes for all other values.

Colors are the identity of the application. Even if you pick a minimalistic color palette, having multiple grey variations can still look bad even to the untrained eye.

A button, for example, probably needs multiple colors for its normal, hovered, pressed, and disabled states. Now, this component would look a lot better if all these colors were different shades of the same base primary color.

Design Tokens

We can’t reliably reuse classes, but we can reuse the CSS values. It’s hard to reuse a complex element’s CSS, but all of them can be underpinned by the same set of “design tokens”.

Design tokens are atomic values that represent the smallest units of a design system - colors, font sizes, spacings, animations, and everything else we’d need to reuse. Contrary to component libraries which keep together both visual and functional (things), design tokens carry only the styles.

They aim to abstract away the decisions about picking the correct values when implementing components and help us to maintain consistency. In modern browsers, we can use CSS variables to define these values.

:root {
  /* Color variables */
  /* HSL allows us to make easier modifications to brightness */
  --color-primary: hsl(221, 44%, 41%);
  --color-primary-light: hsl(221, 32%, 65%);
  --color-primary-dark: hsl(211, 50%, 29%);
  /* But hardcoded hex values are also fine */
  --color-grey: #65737e;
  --color-grey-light: #c0c5ce;
  --color-grey-dark: #343d46;
  --color-black: #1a1a1a;
  /* RGBA too */
  --color-white: rgba(255, 255, 255, 0.9);

  /* Font family variables */
  --font-serif: 'Merriweather', serif;
  --font-sans-serif: 'Montserrat', sans-serif;

  /* Font size variables */
  --font-size-2xs: 0.75rem;
  --font-size-xs: 0.875rem;
  --font-size-sm: 1rem;
  --font-size-md: 1.125rem;
  --font-size-ml: 1.5rem;
  --font-size-lg: 2.25rem;
  --font-size-xl: 3rem;
  --font-size-2xl: 3.75rem;

  /* Spacing variables */
  --space-2xs: 0.25rem;
  --space-xs: 0.5rem;
  --space-sm: 0.75rem;
  --space-md: 1rem;
  --space-ml: 1.25rem;
  --space-lg: 2rem;
  --space-xl: 3.25rem;
  --space-2xl: 5rem;
}

By using CSS variables we remove a lot of decision-making and argument-fuel from the application. Everyone is operating with the same set of creative constraints and they need a good argument to break out of it.

Reuse Components Not Styles

I noticed that every time I need to reuse a class, I’m actually trying to reuse a component. When I’m reusing the CSS for a button, I’m not putting it on any other element, I’m putting it on a button. The same goes for input fields, layouts, and any custom components I’ve built.

I’m reusing the component, not the class.

But how is this different? A component is a complete cohesive unit that comes with its styling and functionality. It also solves, the intellisense problem of class hierarchies - we can communicate what can be tweaked in the component through its props.

Thinking in components

Take our .highlighted class that we pondered so much at the beginning of the article. Its presence reflects the need for a prop passed to the component. This is yet another example of how coupled styles and markup are.

But if we misuse a component’s API we will get an error.

When I started thinking about components instead of markup and styles I started seeing their separation as unnecessary friction. They described the same entity and that was visible in the coupling. The CSS solution I needed was one that didn’t rely on semantic classes.

CSS-in-JS

Most elements on the screen don’t have event handlers or domain logic related to them, they’re styled pass-throughs, React components whose only responsibility is to accept children and render them.

Some front-end developers realized the tight coupling between styles and markup and decided to create tools that fully lean into that. CSS-in-JS libraries provide us with a shorthand API to create a component and style it at the same time.

Now instead of the .text-box class we had above, we will have a set of components representing each part of it.

const TextBox = styled.div`
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;
`

const TextBoxTitle = styled.div`
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 15px;
`

const TextBoxContent = styled.div`
  font-size: 16px;
  margin-bottom: 10px;
`

const TextBoxAuthor = styled.div`
  display: flex;
  align-items: center;
`

const TextBoxAuthorImage = styled.img`
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
`

const TextBoxAuthorName = styled.div`
  font-size: 14px;
  font-style: italic;
`

export function TextComponent() {
  return (
    <TextBox>
      <TextBoxTitle>What is the meaning of life?</TextBoxTitle>
      <TextBoxContent>42</TextBoxContent>
      <TextBoxAuthor>
        <TextBoxAuthorImage
          src="/path/to/image.jpg"
          alt="Alex Kondov"
        />
        <TextBoxAuthorName>Alex Kondov</TextBoxAuthorName>
      </TextBoxAuthor>
    </TextBox>
  )
}

We export our entire TextComponent, presenting it as a cohesive unit, keeping an abstraction over its building blocks. This communicates that the component is only reusable as a whole.

We don’t have to think of semantic classes because the name of the component describes its purpose. Notice that we’re not using nested styles, but describing each component on its own, decoupling them from their place in the component.

Utility Classes

CSS-in-JS are one way to couple markup to styles so we can work with components. Another one, that may seem counterintuitive at first are utility classes.

function TextComponent() {
  return (
    <div className="border border-black rounded p-2.5 mb-1.5">
      <div className="text-2xl font-bold mb-4">Title of the Box</div>
      <div className="text-lg mb-2.5">
        Content of the box goes here.
      </div>
      <div className="flex items-center">
        <img
          src="/path/to/image.jpg"
          alt="Author"
          className="w-12 h-12 rounded-full mr-2.5"
        />
        <div className="text-base italic">Author Name</div>
      </div>
    </div>
  )
}

This example uses Tailwind classes to achieve the same result as our previous examples and its implementation is the most condensed one so far. Instead of defining separate components, we’re once again reaching for classes, but this time they’re not semantic.

We’re using shorthand styles to style each element. If the classic approach to styling is similar to inheritance in programming, this one is the equivalent of composition, and I’ve found that the latter works a lot better in the context of CSS.

Coupling Markup, Styles and Design Tokens

The more things we depend on peer reviews for, the higher the chance something slips unnoticed. The biggest offender here is design tokens.

A team agrees to use a pre-selected color palette, spacings, and fonts only to find their codebase riddled with magical values because people were in a rush or they couldn’t find the proper value to use. Design tokens add a little bit of friction since you have to look for the proper one to use.

With Tailwind’s utility classes, using a design token is part of the style itself. Deciding font sizes, margins, paddings, and colors is baked into the utility class, reducing the friction as much as possible.

It’s also a lot easier to write styles since you’re not jumping between multiple files - you’re writing the markup and the CSS in one go.

What Does Scale Mean?

I’m not affiliated with Tailwind in any way, but I’ve found the utility class approach to styling to be the most scalable. In the context of CSS, scalability means being able to add more to the page without increasing the styling effort proportionally.

The styling effort with utility classes is always fixed. There’s no chance of one developer unintentionally changing the look of another component by updating a class.

There’s no reusability to think about, no design tokens to enforce in code reviews. The reason behind using semantic classes is that they describe the content they’re labeling, but we saw that it’s not as simple as that.

Tying class semantics to the nature of the content already hurts scalability.

The only reusable components are those whose names are independent of their content. This doesn’t mean that these class names are not semantic. It just means their semantics are not tied to the content.

What Does Semantic Mean?

Semantics are about the agreed meaning of something, a shared understanding.

Semantic HTML means that we write markup in a way it’s expected to be written, so other engineers and tools can understand it. We’ve agreed that heading elements are labeled h1 to h6, and that button should be used to submit forms, not a div.

But when it comes to CSS there’s no semantics. There’s no agreed meaning behind the classes. No one has described what a .text-box is or what a .card is or a .card-title. These are all meanings we give to the classes.

In that sense, classes can’t be unsemantic. You can’t write a class that lacks meaning because you’re the person giving it to it.

And even though the HTML spec says that developers are encouraged to use classes that describe the content, there are no specific reasons why this advice should apply to modern front-end development that’s long strayed away from older practices.

Semantic Classes are Better

Is .text-box a better class than border border-black rounded p-2.5 mb-1.5? Of course it is.

But my goal when writing CSS isn’t to write good classes. It’s to style a product in a way that will help me work with it in the future.

One day after I write the .card-header class I will have no idea about its exact styles just by looking at the code. Margins, paddings, font-size - unless I hop to the CSS files I won’t have any idea about what it does.

Now, when I see the other class I can understand what styles it has. But more importantly, I will be able to understand them a month or a year from now when I have to make a quick change to the project.

Making CSS Changes

The way we write CSS and the way we change it are a lot different.

When we’re writing CSS the classic way we think of the best way to describe the content with classes. When we need to change something we rarely think about updating the class design if it’s not good enough anymore.

Usually, we pinpoint the place we need to modify with the help of the console and apply a surgical style change. There’s no point in having a complex class hierarchy if we’re not taking advantage of it after the time of writing.

I’m yet to see someone tracing a style properly through semantic classes. With utility classes, on the other hand, the change we need to make is always inside the component.

A Classless Styling Philosophy

My styling philosophy is one that leaves classes in the past as a no-longer-needed tool to build complex UIs. Separation of concerns is important, but in modern front-end development, the concern is the component.

To understand why I appreciate this approach so much we need to look back at how CSS was written before component-based libraries existed.

Before React, Angular, and everything else that makes our lives easier, we had miles-long HTML files that described entire pages of a website.

Tags, new lines, and indentation are good if you need to figure out where an element begins and where it ends. But that still left a couple of problems - selecting elements with CSS based on tags was a nightmare, and figuring out the meaning of each element in the long HTML file was impossible.

So classes were our solution.

A deeply nested HTML was nigh impossible to decipher if it wasn’t for classes. They solved both problems by giving a simple way to select a specific element with CSS based on its class and providing a purpose to every element.

Going through the markup became a lot easier when every tag had a name attached to it and you could figure out what it was used for.

But for reasons too long to describe here we’re now working with components, not pages.

Our development flow became much easier since we could focus on one element at a time, thinking about the data it needs and the styles that need to be applied to it.

But that was a bigger paradigm shift than most developers imagined. Suddenly, the two problems that underpinned our entire styling philosophy no longer existed. Selecting elements and making sense of them was a lot easier when we only dealt with a fraction of the UI at a time.

Semantic classes are no longer needed to solve these problems.

We talk a lot about limiting complexity, and having utility classes inside components is a lot simpler than any other alternative we have.

Handling Complexity with Utility Classes

Our component above was pretty straightforward because it wasn’t taking in any props, but the moment it becomes more complex, wouldn’t utility classes be too under-engineered as a solution?

There are a few ways we can handle higher complexity with them.

Props that affect a component’s style are reflected as a change to the component’s utility classes. So inlining a conditional in the className props is the simplest way to do this.

function TextComponent({ highlighted }) {
  return (
    <div
      className={`border border-black rounded p-2.5 mb-1.5 ${
        highlighted ? 'bg-black' : ''
      }`}
    >
      // ...
    </div>
  )
}

But this will quickly get out of hand. Our opening tag now takes 5 lines just because of one conditional so we can imagine what would happen if we have to add another two. An alternative is to construct the classes using a library like classnames.

function TextComponent({ highlighted }) {
  const classes = classNames(
    'border',
    'border-black',
    'rounded',
    'p-2.5',
    'mb-1.5',
    { highlighted }
  )
  return <div className={classes}>...</div>
}

But isn’t this a lot lengthier? Yes, but one thing we need to keep in mind is that the elements that require any dynamic classes applied to them are a fraction of the entire application. And on top of that, we’re working with components, so we only need to manage this complexity once.

If other elements inside the component need conditional logic applied to them, I would do what I would do with any other lengthy function - extract another one.

Styling is complex, and extracting a component so we can manage some of it is absolutely fine.

Complex Classes

There are components that won’t just change a class or two, though.

function TextComponent({ highlighted, large, disabled, error }) {
  const classes = classNames(
    'border',
    'border-black',
    'rounded',
    'p-2.5',
    'mb-1.5',
    {
      highlighted: highlighted,
      'bg-yellow-200': highlighted && !error,
      'bg-red-200': highlighted && error,
      'text-lg': large,
      'opacity-50': disabled,
      'cursor-not-allowed': disabled,
      'font-bold': highlighted || large,
      'border-red-500': error,
      'hover:bg-gray-100': !disabled,
    }
  )

  return <div className={classes}>...</div>
}

When we find ourselves in this spot, another technique worth considering is splitting it into separate components. A function should have a single responsibility, and if a component is being too flexible it means it’s doing too many things.

A common practice is splitting off the base in another component, leaving only the configurable bits inside the original one.

function StyledTextComponent({
  highlighted,
  large,
  disabled,
  error,
  children,
}) {
  const styledClasses = classNames({
    'bg-yellow-200': highlighted && !error,
    'bg-red-200': highlighted && error,
    'text-lg': large,
    'opacity-50': disabled,
    'cursor-not-allowed': disabled,
    'font-bold': highlighted || large,
    'border-red-500': error,
    'hover:bg-gray-100': !disabled,
  })

  return (
    <BaseTextComponent className={styledClasses}>
      {children}
    </BaseTextComponent>
  )
}

Component Architecture, Not CSS Architecture

The popular CSS paradigms are getting us to focus on structuring our CSS, but I’d rather styles be a part of our component design efforts rather than something separate to think of.

We should have component architecture, not styling architecture.

I don’t want this chapter to make it sound like styling is the hardest thing in the world. Bad CSS will leave your page looking a bit broken, it might rile up a customer or in the absolute worst case lose you some money. A database error, though, could ruin your whole company.

Yes, complex UIs, animations, and layouts are always a challenge, but the vast majority of web applications don’t need that. What we need is a common sense approach to writing CSS that ties well with the existing tools we’re using.

We’re already thinking in components when it comes to state management. It’s natural to do it with styling too.

I don’t want to think about CSS architecture. I want to be able to style my components in a manner that will allow me to work productively and focus on the critical aspects of the application.

Tao of Node

Learn how to build better Node.js applications. A collection of best practices about architecture, tooling, performance and testing.