Using Styled Components in your project can be a wonderfully efficient way to compartmentalize the different CSS within your React project. However, as a project grows in size and complexity, I’ve often discovered that some foundational styled-components
start to feel rigid and fragile. Whether it’s finding myself in a position where I have to duplicate the single “DRY” page-agnostic `
I often find myself thinking, “If I ever need to revisit this, I’ll have to change it in multiple places instead of just one…” 🤦♂️ or “Embedding that function within the styled component turned what was once straightforward CSS into a confusing mess.” Whenever a section of code becomes difficult to read or understand—especially after running it through prettier—I realize it’s likely to resurface as a bug or a time-waster. We often underestimate how quickly these improvised solutions can spread and replicate throughout a project. What starts as a temporary fix or hack evolves into an alternate way of doing things, effectively doubling the mental effort needed to fully grasp it.
After observing a gradual yet elusive decline in several previous projects, I have identified a few strategies to help us craft clean, maintainable, and well-stylized components.
The initial point may seem self-evident, but it is nonetheless important to highlight. Using the same static hex color values, font sizes, widths, or padding measurements repeatedly across 5 to 30 different Styled Components can quickly become cumbersome as the project expands. A practical starting point for addressing this issue in any project, whether it’s new or existing, is to organize the color palette. Consolidate all color codes into a shared object for better manageability.
const export COLORS = {
transparent: 'transparent',
white: '#fff',
black: '#000',
text:'#050810',
background: '#fefcfa',
primary: '#211F1E',
secondary: '#2A2827',
// ... Depending on specificity requirements, borrowing from TailwindCSS structure.
gray: {
100: '#f6f6f6',
200: '#edeef0',
300: '#dfe1e4',
400: '#cbd5e0',
500: '#9da7b7',
600: '#65728a',
700: '#555555',
800: '#2d3748',
900: '#1a202c',
},
}
You can slowly expand on this to include all types of variables that can then be re-used across the entire project. This goes a long way to making future updates a breeze. Generally, I end up with a global config that could look something like the following on every project…
const config = {
default: {
title: 'My Project',
logo: 'https://myproject.com/favicon/favicon-512.png',
author: 'John Smith',
url: 'https://myproject.com',
api: 'https://myproject.com/api/v2',
defaultDescription: 'Short description of my project.',
googleAnalyticsID: '',
social: {
email: 'mailto:email@myproject.com',
facebook: '',
twitter: '',
github: '',
linkedin: '',
instagram: '',
},
routes: {
posts: 'posts',
post: 'post',
projects: 'projects',
project: 'project',
tags: 'tags',
categories: 'topics',
},
theme: {
primary: '#511281',
secondary: '#FFD371',
black: '#1a1917',
primaryBg: '#21094E',
white: '#fff',
lightBg: '#efeeff',
darkBg: '#212121',
gray: '#707070',
lightGray: '#c7c7c7',
darkGray: '#3d4852',
hightlight: '#E9EDD3',
error: '#FF4848',
success: '#A5E1AD',
info: '#ACDDDF',
},
features: {
core: true,
alerts: true,
},
...
},
development: {
googleAnalyticsID: 'UA-devopment-key',
...
},
production: {
googleAnalyticsID: 'UA-production-key',
...
features: {
core: true,
alerts: false, // disable target functionality based on current ENV.
}
}
};
module.exports = {
config: {
...config.default,
...config[process.env.NODE_ENV],
},
completeConfig: config,
};
Personally, I consider the freedom to name my stylized components as I wish to be both a blessing and a curse. When I initially began working with React, my components would typically look something like this…
// v1
const ListOfStuff = () => {
// Array of stuff we're going to render.
const items = [...];
return (
<Wrapper>
<List>
{items.map((item) => (
<ListItem>
<ItemHeader>{item.header}</ItemHeader>
<ItemContent>{item.content}</ItemContent>
<ItemFooter>
<ItemPublished>{item.datePublished}</ItemPublished>
<ItemLink href={item.url}>View Item</ItemLink>
</ItemFooter>
</ListItem>
))}
</List>
</Wrapper>
)
}
While this is perfectly functional, I’d posit it’s aggressively over-complicated, redundant, and most importantly obfuscates what typically would be straightforward HTML markup behind a subjective naming convention.
No matter how far we’ve advanced it’s important not to forget where we started. CSS is more than capable of targeting each element in this component without having to create a unique styled component for it.
Instead, we can write our necessary styles within a single Wrapper Component. This allows us to keep our CSS short and simple, targeting the elements themselves, without having to worry about global scope pollution.
// v2 - Clean things up a bit.
const ListOfStuff = () => {
// Array of stuff we're going to render.
const items = [...];
return (
<Wrapper>
<ul>
{items.map((item) => (
<li>
<h3>{item.header}</h3>
<p>{item.content}</p>
<div>
<span>{item.datePublished}</span>
<a href={item.url}>View Item</a>
</div>
</li>
))}
</ul>
</Wrapper>
)
}
As components become more complex, we can incorporate additional styled components to better compartmentalize specific parts of the markup and styling. This approach of dividing larger components with styled components is typically the initial step in refactoring functionality into smaller, more manageable components.
// v3 - Mix in classes for convenience.
const ListOfStuff = () => {
// Array of stuff we're going to render.
const items = [...];
return (
<Wrapper>
<ul className="list">
{items.map((item) => (
<li className="item">
<h3 className="item__header">{item.header}</h3>
<p className="item__content">{item.content}</p>
<div className="item__footer">
<span className="item__published">{item.datePublished}</span>
<a href={item.url}>View Item</a>
</div>
</li>
))}
</ul>
</Wrapper>
)
}
The only difference with the final version below is that additional class names have been added to the HTML tags. One of my favorite parts is this step is often unnecessary and can get away with vanilla HTML. If we don’t need a class for styling or targeting, don’t add it 🤷♂️
The convenience and power of passing stateful props into stylized components resulting in dynamic CSS rules are, in my opinion, one of the strongest things Styled Components has going for it. Before React this typically would have been done by juggling a few different classes e.g. is-active
, is-desktop
, is-light-theme
, is-rounded
. Variants upon variants.
Enter Styled Components, offering an intuitive path to providing dynamic styles on a rule-by-rule basis while applying to whatever context you desire, be it global or local. Given that this is likely something you’re already familiar with I’m not going to go into much detail on passing props into Styled Components, as many others have already done so here, here, and here.
I generally adhere to a simple guideline: use props to create dynamic styles only when it’s a binary decision. If a component evolves to support multiple variations with different props, it can be more efficient to break it into separate styled components
and utilize the `as
` prop instead. Consider the example component below:
Below is an example of a real-world styled component that started its life as a basic SVG wrapper, but it wasn’t long before we needed to control multiple properties on a case-by-case basis in addition to a :hover
state.
import styled from 'styled-components';
// Applies dynamic CSS color styles to SVG child node.
const SvgWrapper = styled.span`
svg {
display: inline;
}
${({ iconProps, hoverProps }) => {
const { fill, stroke, transform } = iconProps;
let styles = '';
// Root level styles
styles += fill ? `fill: ${fill}!important;` : '';
styles += stroke ? `stroke: ${stroke}!important;` : '';
styles += transform ? `transform: ${transform};` : '';
// TODO: This should have some better validation to prevent against invalid CSS.
// Could result in invalid rules if bad object keys is passed.
Object.keys(iconProps).map((elementName) => {
const {
childStroke, childFill, childTransform, childBackground, childBorderRadius,
} = iconProps[elementName];
styles += `
${elementName} {
${childFill ? `fill: ${childFill}!important;` : ''}
${childStroke ? `stroke: ${childStroke}!important` : ''}
${childTransform ? `transform: ${childTransform}!important` : ''}
${childBackground ? `background: ${childBackground}!important` : ''}
${childBorderRadius ? `border-radius: ${childBorderRadius}!important` : ''}
}
`;
// Appease linters.
return true;
});
Object.keys(hoverProps).map((elementName) => {
const { hoverStroke, hoverFill, hoverTransform } = hoverProps[elementName];
styles += `&:hover {
${elementName} {
${hoverFill ? `fill: ${hoverFill}!important;` : ''}
${hoverStroke ? `stroke: ${hoverStroke}!important` : ''}
${hoverTransform ? `transform: ${hoverTransform}!important` : ''}
}
}`;
// Appease linters.
return true;
});
return styles;
}};
`;
So while this is perfectly functional, and currently deployed to live applications, I’ve learned that maintaining the component below is very rarely worth the 👩🎤✨cool points✨👨🎤 one feels when initially crafting it.
Despite its straightforward purpose, it’s difficult to quickly apprehend what and how it operates. Simply put, if writing JavaScript inside of CSS is generally considered a messy sacrilegious behavior then this component would be par to pissing on the steps of the Vatican.
Despite the uncompromising power that can be achieved when passing custom props to our Styled Components, as soon as are juggling more than two possible states within a styled component it generally is a signal that each business requirement should be isolated into a separate component.
This is where I’ve found using the as
prop to be especially powerful by providing a much cleaner strategy for handling variants through inheritance. Let’s take a look at the following chunk of code where we are passing a common Container
component using as={Container}
, which provides our underlying set of CSS for all containers, and the locally scoped <Wrapper>
component comes in to provide page-specific styles, e.g., padding, backgrounds, etc.
as
` propBut then how do we benefit from all the re-usability that a set of common styled components offer? I want my one-size-fits-all
Layout
component!
Take the following stylized component that provides a standardized content container for the entire application.
import styled from 'styled-components';
export const Container = styled.div`
max-width: 1280px;
padding: 2rem 0 3rem;
margin: 0 auto;
width: 90%;
@media (min-width: 601px) {
width: 90%;
}
@media (min-width: 993px) {
width: 80%;
}
`;
This element is typically used on every page as a content wrapper. However, complications arise when the next page we design requires a width of 100% or perhaps additional vertical padding, such as padding: 16rem 0 4rem. My initial instinct when I first faced this issue was to extend the base <Container>
component to accept additional properties, such as padding and width. But at what point does this approach become cumbersome and confusing? The previously clean and readable CSS rules for our <Container>
component can quickly devolve into a chaotic tangle.
// src/components/common/Container/index.js
import styled from 'styled-components';
export const Container = styled.div`
max-width: 1280px;
margin: 0 auto;
width: 90%;
@media (min-width: 601px) {
width: 90%;
}
@media (min-width: 993px) {
width: 80%;
}
`;
This Container
will provide all the basic layout constraints we’ll be using on all pages. The following Wrapper
component is going to provide any overrides or page-specific styles.
Note: Styled Components passed in via as
prop are evaluated with a lower CSS specificity than any styles included inside the main Wrapper
Component.
// src/components/MyComponent/styles.js
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 4rem 0;
`;
// src/components/MyComponent/index.js
import React from 'react';
import { Container } from "components/common/Container"
import { Wrapper } from "./styles.js"
export const MyComponent = () => (
<Wrapper as={Container} id="activity">
<h2>My Special Component</h2>
// page markup...
</Wrapper>
);
};
Ultimately, we are provided with a versatile strategy for developing a collection of universal, impartial, and reusable components. By utilizing the as
keyword, we can seamlessly inherit these properties into our locally scoped components. Presented below is a snapshot of a project directory that encompasses both a common suite of styled components and styles tailored for individual pages.
src/ assets/ ... components/ common/ Container/ index.js styles.js Button/ index.js styles.js forms/ Contact/ index.js styles.js Logo/ index.js styles.js Layout/ index.js styles.js components/ Header/ index.js style.js Footer/ index.js style.js sections/ Hero/ index.js styles.js services/ ... pages/ /about index.js styles.js /contact index.js style.js
As already mentioned, writing JavaScript inside of a block of CSS is, and will forever be, fugly. It’s a means to an amazingly powerful end, however, it often has the adverse side effect of turning one of the most readable languages into a barrage of brackets, backticks, and nested ternaries. One of the biggest and most obvious aids in this fight against the cascade is a utils/
folder full of helper functions.
TODO: Helper Functions
Here’s where things really get turned up to 11 if you’ve ever had to work with a project which supports something like a light/dark theme and each styled component needs to be aware of the active theme in order to appropriately apply its rules.
Create helper hooks that allow you to avoid prop-tunneling e.g. with light/dark themes
/**
* Provides theme dependant A/B variable within Styled Components.
*
* @param {string} lightVariable String to use in CSS when using light theme.
* @param {string} darkVariable String to use in CSS when using dark theme.
* @returns
*/
const useThemeVar = (lightVariable, darkVariable) => {
const { theme } = useTheme();
if (!lightVariable || !darkVariable) {
return 'inherit';
}
return theme === 'light' ? lightVariable : darkVariable;
};
Then in our styles…
export const ThemedContainer = styled.div`
background: ${() => useThemeVar(config.theme.lightBg, config.theme.darkBg)};
`;
In conclusion, writing maintainable Styled Components involves a strategic approach to organization and scalability. By avoiding the hard coding of common values and centralizing them into shared objects, you ensure consistency and simplicity when updates are needed. Establishing a global config further curtails redundant declarations and fosters a more cohesive project structure. Naming conventions are another crucial consideration; simplicity aids in readability and prevents unnecessary complexity.
The `as` prop offers a clean and efficient method for leveraging inheritance, allowing for the easy extension of styles without overcrowding your components with excessive props.
Finally, helper functions and hooks serve as essential tools to retain the cleanliness and manageability of your styles, making them adaptable and resilient to changes such as theme shifts. Implementing these practices not only enhances the maintainability of your styled components but also supports a more productive and streamlined development process.