Dynamic Styling with styled-components: Props & Variants
The real power of styled-components lies in dynamically adapting styles based on props. By passing functions inside template literals, you can read component props and change CSS properties—enabling reusable component variants, themes, and responsive UI patterns without defining multiple CSS classes.
How styled-components Reads Props
Because styled-components are real React components, they accept props like any other component. Inside the CSS template literal, you can pass a function that receives those props and returns a CSS value. This allows your styles to respond to component state and parent configuration.
const StyledDiv = styled.div`
background-color: ${props => (props.primary ? 'blue' : 'white')};
`;
In this example, the background-color property evaluates at render time. If <StyledDiv primary /> is rendered, the background becomes blue; otherwise, it's white. This declarative approach keeps styling and logic tightly coupled and readable.
Dynamic Button with Prop-Based Styling
Let's build a real Button component that changes appearance based on a $primary prop. The dollar-sign prefix ($) is a styled-components convention that marks a prop as "transient"—used only for styling, not passed to the underlying DOM element.
import React from 'react';
import styled from 'styled-components';
const Button = styled.button`
/* Adapt the colors based on a $primary prop */
background: ${props => (props.$primary ? '#BF4F74' : 'white')};
color: ${props => (props.$primary ? 'white' : '#BF4F74')};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid #BF4F74;
border-radius: 3px;
cursor: pointer;
`;
function App() {
return (
<div>
<Button>Normal Button</Button>
<Button $primary>Primary Button</Button>
</div>
);
}
export default App;
Code Breakdown:
background: ${props => ...}: Inside the CSS block, the${...}interpolation executes a function.props => (props.$primary ? ...): The function receives the component'spropsand uses a ternary operator to checkprops.$primary.<Button $primary>: When rendering, you pass the$primaryprop. The dollar sign signals that this prop is for styling only—it won't appear on the actual<button>HTML element.
This pattern is declarative: you specify the UI state (primary or not) through props, and styles adapt automatically. No extra CSS classes, no conditional class names—just clean, JavaScript-driven styling.
Why Use Transient Props ($)?
Transient props prevent your styled component from passing styling-only props to the underlying DOM element. Without the $ prefix, a prop like primary would be passed to <button primary>, which is invalid HTML. With the $, only valid HTML attributes reach the DOM.
// Without $ — 'primary' appears in HTML (bad!)
<button primary>Click me</button>
// With $ — 'primary' is consumed by styled-components (good!)
<button>Click me</button>
Always prefix styling-only props with $ to keep your rendered HTML clean and valid (Web Accessibility Guidelines, 2025).
Extending Styles: Building Component Variants
For static variations (not dynamic), styled-components offers a cleaner pattern: extending styles. Create a new styled component based on another by passing the original to the styled() constructor.
import React from 'react';
import styled from 'styled-components';
const Button = styled.button`
color: #BF4F74;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid #BF4F74;
border-radius: 3px;
`;
// Create a new component that extends the Button
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;
function App() {
return (
<div>
<Button>Normal Button</Button>
<TomatoButton>Tomato Button</TomatoButton>
</div>
);
}
export default App;
The TomatoButton inherits all CSS from Button (padding, margin, font-size, etc.). The styles you define inside TomatoButton override inherited properties. This is the foundation of scalable component libraries: define a base component, then extend it for specific themes or use cases.
Combining Props and Extends for Flexible Theming
For maximum reusability, combine dynamic props with style extending. Here's a complete example:
const BaseButton = styled.button`
font-size: 1em;
padding: 0.5em 1em;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
`;
const PrimaryButton = styled(BaseButton)`
background: ${props => (props.$dark ? '#1a1a1a' : '#0066cc')};
color: white;
&:hover {
opacity: 0.9;
}
`;
const SecondaryButton = styled(BaseButton)`
background: ${props => (props.$dark ? '#555' : '#ddd')};
color: ${props => (props.$dark ? '#fff' : '#000')};
&:hover {
opacity: 0.8;
}
`;
This approach gives you both reusability (extend a base component) and flexibility (use props to customize colors, sizes, etc. at runtime).
Best Practices for Dynamic Styling
- Use Transient Props (
$): Mark styling-only props with a$prefix so they don't pollute the DOM. - Keep Logic Simple: For complex conditional styling, create a helper function outside the styled component to compute the CSS value.
- Props for Runtime Variants: Use props when the styling changes based on user interaction, state, or data (e.g.,
$primary,$disabled). - Extends for Static Variations: Use
styled(Component)to create new components that are permanent theme variations (e.g.,TomatoButton,DarkMode). - Leverage
&for Pseudo-Classes: Use&:hover,&:focus, and&:activeto keep related styles together and avoid naming conflicts.
Key Takeaways
- Props Drive Styles: Functions inside
${...}interpolations receive props and return CSS values. - Transient Props Stay in JS: Prefix styling-only props with
$to prevent them from reaching the DOM. - Dynamic = Props, Static = Extends: Use props for runtime variations, extending styles for permanent theme variants.
- Declarative and Maintainable: Styling logic stays with the component, making code easier to read and refactor.
- Performance:
styled-componentsgenerates unique class names and injects CSS dynamically, enabling dead code elimination and avoiding style conflicts.
Frequently Asked Questions
What is the difference between a transient prop and a regular prop?
A transient prop (prefixed with $, like $primary) is used only for styling and is not passed to the underlying DOM element. A regular prop (like aria-label) is passed through to the DOM. Use $ for styling-only props to keep your HTML clean and valid.
Should I always extend styles or use props for variants?
Use extends for static, predefined variations (like DarkButton, SmallButton) that are fixed at definition time. Use props for dynamic variations that change based on runtime state or user actions (like $disabled, $loading). Combine both for maximum flexibility.
Can I use media queries with styled-components?
Yes. Write media queries directly in your template literal, and use props to control breakpoint behavior:
const ResponsiveDiv = styled.div`
width: 100%;
@media (min-width: 768px) {
width: ${props => (props.$wide ? '80%' : '50%')};
}
`;
What happens if I don't use $ for styling props?
Without the $ prefix, the prop is passed to the underlying DOM element. For custom props like primary or variant, this results in invalid HTML attributes (the browser ignores them). For reserved words like className, it causes unexpected behavior. Always use $ for styling-only props.
How do I handle complex conditional styles?
Move the logic to a helper function outside the styled component:
const getButtonBackground = (props) => {
if (props.$primary) return '#0066cc';
if (props.$danger) return '#cc0000';
return '#ccc';
};
const Button = styled.button`
background: ${getButtonBackground};
`;