Styling Components, Minus the CSS Hassle
Styled components are billed as “visual primitives for components” — a way to define scoped, reusable styles that live beside the components they style. This fits neatly into the component-driven approach, where colocation keeps assets (state, markup, and now styling) close together.
The core issue it addresses is CSS’s global nature. Targeting any HTML element anywhere in the DOM is great for documents, but a poor match for components that need localized styling. Styled components solve this by bringing styles into components, and as a functional abstraction, they can adapt to props and state just like compute in React. The library works for React and React Native, though this article focuses on React usage.
Beyond scoping, styled components bring a few technical wins:
- Automatic vendor prefixing — write standard CSS properties; the library handles prefixes when needed.
- Unique class names — no collisions, no naming decisions; the library generates them.
- Dead-style elimination — unused styles are removed, even if declared in code.
Setup and First Steps
You can install styled components through a CDN or with a package manager. Using Yarn:
yarn add styled-components
Or npm:
npm i styled-components
This demo assumes a project scaffolded with create-react-app.
Styled components lean on JavaScript template literals. When you create one, you are building a React component that carries its own styling:
import styled from "styled-components";
// Styled component named StyledButton
const StyledButton = styled.button`
background-color: black;
font-size: 32px;
color: white;
`;
function Component() {
// Use it like any other component.
return <StyledButton> Login </StyledButton>;
}
In that snippet, StyledButton renders as a button element with the defined CSS, and styled is the internal utility converting JavaScript into CSS. In raw markup, this would be the equivalent:
button {
background-color: black;
font-size: 32px;
color: white;
}
<button> Login </button>
The real power shows when you start passing props — a styled component behaves like any other React component.
Conditional Styling, Event Handlers, and Defaults
Say you need two button variants, one black and one blue. Instead of creating two separate styled components, you can adapt one based on a bg prop location and value:
import styled from "styled-components";
const StyledButton = styled.button`
min-width: 200px;
border: none;
font-size: 18px;
padding: 7px 10px;
/* The resulting background color will be based on the bg props. */
background-color: ${props => props.bg === "black" ? "black" : "blue";
`;
function Profile() {
return (
<div>
<StyledButton bg="black">Button A</StyledButton>
<StyledButton bg="blue">Button B</StyledButton>
</div>
)
}
Passing props works further: styled components recognize which props are valid HTML attributes. For example, type is rendered to the DOM as <button type="button">Button A</button>, while the bg prop is only used for processing — and note that event handlers like onClick attach directly. The attrs constructor extends prop management even more:
function Profile() {
return (
<>
<StyledButton bg="black" type="button">
Button A
</StyledButton>
<StyledButton bg="blue" type="submit" onClick={() => alert("clicked")}>
Button B
</StyledButton>
</>
);
}
Using attrs, you can define defaults. This next snippet sets a fallback width via props.width || "100%", so the need for a ternary disappears. It also takes advantage of CSS custom properties:
const StyledContainer = styled.section.attrs((props) => ({
width: props.width || "100%",
hasPadding: props.hasPadding || false,
}))`
--container-padding: 20px;
width: ${(props) => props.width}; // Falls back to 100%
padding: ${(props) =>
(props.hasPadding && "var(--container-padding)") || "none"};
`;
Building on Styles
If you work on a landing page with a container capped at a certain width to stay centered, you likely start with something like this:
const StyledContainer = styled.section`
max-width: 1024px;
padding: 0 20px;
margin: 0 auto;
`;
When you later need a smaller version with tighter padding, resist the temptation to duplicate:
const StyledContainer = styled.section`
max-width: 1024px;
padding: 0 20px;
margin: 0 auto;
`;
const StyledSmallContainer = styled.section`
max-width: 1024px;
padding: 0 10px;
margin: 0 auto;
`;
Instead, inherit and override. It works much like the spread operator:
const StyledContainer = styled.section`
max-width: 1024px;
padding: 0 20px;
margin: 0 auto;
`;
// Inherit StyledContainer in StyledSmallConatiner
const StyledSmallContainer = styled(StyledContainer)`
padding: 0 10px;
`;
function Home() {
return (
<StyledContainer>
<h1>The secret is to be happy</h1>
</StyledContainer>
);
}
function Contact() {
return (
<StyledSmallContainer>
<h1>The road goes on and on</h1>
</StyledSmallContainer>
);
}
Any style from StyledContainer is carried over, only the padding gets overridden. The as polymorphic prop gives you control over the rendered element. If you prefer a div over a section here (as StyledContainer suggests), pass the desired element to as:
function Home() {
return (
<StyledContainer>
<h1>It’s business, not personal</h1>
</StyledContainer>
);
}
function Contact() {
return (
<StyledSmallContainer as="div">
<h1>Never dribble when you can pass</h1>
</StyledSmallContainer>
);
}
You can even pass a custom component, not just a DOM element:
function Home() {
return (
<StyledContainer>
<h1>It’s business, not personal</h1>
</StyledContainer>
);
}
function Contact() {
return (
<StyledSmallContainer as={StyledContainer}>
<h1>Never dribble when you can pass</h1>
</StyledSmallContainer>
);
}
SCSS, Animation, and Global Styles
Styled components support SCSS-like syntax via the Stylis preprocessor. That includes nesting:
const StyledProfileCard = styled.div`
border: 1px solid black;
> .username {
font-size: 20px;
color: black;
transition: 0.2s;
&:hover {
color: red;
}
+ .dob {
color: grey;
}
}
`;
function ProfileCard() {
return (
<StyledProfileCard>
<h1 className="username">John Doe</h1>
<p className="dob">
Date: <span>12th October, 2013</span>
</p>
<p className="gender">Male</p>
</StyledProfileCard>
);
}
Animated effects are handled with a keyframes helper. The benefit is reusable keyframes detached from styled components, which you can export to include wherever needed:
import styled, {keyframes} from "styled-components";
const slideIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
const Toast = styled.div`
animation: ${slideIn} 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
border-radius: 5px;
padding: 20px;
position: fixed;
`;
Although styled components excel at scoping styles, you can apply global rules too. Writing some global sibling selector would work, but a dedicated createGlobalStyle helper is the intended path, as it can normalize the CSS:
ReactDOM.render(
<StyledApp>
<App />
</StyledApp>,
document.getElementById("root")
);
The points for using createGlobalStyle:
- It allows targeting elements outside the root render, like
htmlandbody. - It injects styles without rendering an HTML element — its purpose is to affect selectors, not specific nodes.
- Styles apply wherever the component exists in the DOM, as long as it is rendered.
Note: styles created this way do not accept children, and are only injected while the component is in the DOM.
If you render GlobalStyle inside a nested component, it will still override selectors intended for a global scope:
import {createGlobalStyle} from "styled-components";
const GlobalStyle = createGlobalStyle`
/* Your css reset here */
`;
// Use your GlobalStyle
function App() {
return (
<div>
<GlobalStyle />
<Routes />
</div>
);
}
import {createGlobalStyle} from "styled-components";
const GlobalStyle = createGlobalStyle`
/* Your css reset here */
.app-title {
font-size: 40px;
}
`;
const StyledNav = styled.nav`
/* Your styles here */
`;
function Nav({children}) {
return (
<StyledNav>
<GlobalStyle />
{children}
</StyledNav>
);
}
function App() {
return (
<div>
<Nav>
<h1 className="app-title">STYLED COMPONENTS</h1>
</Nav>
<Main />
<Footer />
</div>
);
}
The css Helper and StyleSheetManager
Adapting style from props is straightforward for one-off cases. Complex states, however, can produce tangled ternaries. Assign an empty and active state alongside properties like color:
const StyledTextField = styled.input`
color: ${(props) => (props.isEmpty ? "none" : "black")};
`;
Add a “filled” state, and the ternary grows unwieldy:
const StyledTextField = styled.input`
color: ${(props) =>
props.isEmpty ? "none" : props.active ? "purple" : "blue"};
`;
The css helper offers cleaner alternatives for grouping styles:
const StyledTextField = styled.input`
width: 100%;
height: 40px;
${(props) =>
(props.empty &&
css`
color: none;
backgroundcolor: white;
`) ||
(props.active &&
css`
color: black;
backgroundcolor: whitesmoke;
`)}
`;
Even better, break out style groups into organized chunks, which is easier to read and to manage as states expand:
const StyledTextField = styled.input`
width: 100%;
height: 40px;
// 1. Empty state
${(props) =>
props.empty &&
css`
color: none;
backgroundcolor: white;
`}
// 2. Active state
${(props) =>
props.active &&
css`
color: black;
backgroundcolor: whitesmoke;
`}
// 3. Filled state
${(props) =>
props.filled &&
css`
color: black;
backgroundcolor: white;
border: 1px solid green;
`}
`;
For CSS processing tweaks, there’s StyleSheetManager. It accepts props, such as disableVendorPrefixes, that disable vendor prefixing from a particular subtree.
import styled, {StyleSheetManager} from "styled-components";
const StyledCard = styled.div`
width: 200px;
backgroundcolor: white;
`;
const StyledNav = styled.div`
width: calc(100% - var(--side-nav-width));
`;
function Profile() {
return (
<div>
<StyledNav />
<StyleSheetManager disableVendorPrefixes>
<StyledCard> This is a card </StyledCard>
</StyleSheetManager>
</div>
);
}
Its effect applies backward to an entire wrapping <StyleSheetManager>. In the snippet above, the styled components inside the <StyleSheetManager> would be disabled, but <StyledNav> (which sits outside its wrapping) would be unaffected.
Debugging and Bundling
A common complaint when first using styled components is that generated class names are hashes, so locating a rendered element is tough. To mitigate this, styled-components displays readable names in React Developer Tools by default. The class names are still unique for the browser:
import React from "react";
import styled from "styled-components";
import "./App.css";
const LoginButton = styled.button`
background-color: white;
color: black;
border: 1px solid red;
`;
function App() {
return (
<div className="App">
<LoginButton>Login</LoginButton>
</div>
);
}
Toggling the displayName boolean for filename prefixed names requires Babel configuration. The catch is that a create-react-app gives you limited control without ejection. Instead, install babel-plugin-macros (via npm with npm install --save-dev babel-plugin-macros):
module.exports = {
styledComponents: {
displayName: true,
fileName: false,
},
};
Then import styled from the macro and use filename prefixing as needed:
// Before
import styled from "styled-components";
// After
import styled from "styled-components/macro";
Keep Styled-Components Maintainable
Styled-components give you the power to compose CSS programmatically, but that freedom comes with responsibility. Avoid over-engineering your styles with heavy conditionals, and don’t assume every UI element needs to become a styled component. Premature abstraction—creating new components for hypothetical future needs—only adds clutter and complexity.
Learning Resources
To go deeper, start with the official Styled Components documentation, which covers all core APIs and advanced usage. For a practical look at building reusable systems, check out Lukas Gisder-Dubé’s guide on creating a reusable component system with React and styled-components. Framework-specific setup is also well-documented for both Next.js and Gatsby.



