CSS Architecture: BEM, CSS Modules, and Styled Components
Master CSS architecture with BEM methodology, CSS Modules, and Styled Components. Learn when to use each approach and how to write maintainable, scalable styles.
Table of Contents
- Introduction
- The CSS Architecture Challenge
- BEM Methodology
- CSS Modules
- Styled Components
- Comparing the Approaches
- Choosing the Right Approach
- Migration Strategies
- Best Practices and Common Pitfalls
- Conclusion
Introduction
CSS architecture is one of the most critical yet often overlooked aspects of frontend development. As applications grow in complexity, managing styles becomes increasingly challenging. Without a solid architectural approach, you’ll find yourself dealing with naming conflicts, specificity wars, and styles that mysteriously break when you least expect them.
The evolution of CSS architecture has given us several powerful methodologies and tools: BEM (Block Element Modifier) for naming conventions, CSS Modules for scoped styling, and Styled Components for component-based CSS-in-JS solutions. Each approach solves different problems and fits different use cases, but understanding all three will make you a more versatile and effective frontend developer.
This comprehensive guide will teach you everything you need to know about CSS architecture. You’ll learn how BEM provides structure through naming conventions, how CSS Modules solve scoping issues at build time, and how Styled Components enable dynamic, component-scoped styling in React applications. By the end, you’ll be able to choose the right approach for your project and implement it effectively.
The CSS Architecture Challenge
Before diving into specific solutions, it’s important to understand the problems they solve. Traditional CSS has several fundamental challenges that make large-scale styling difficult.
The Global Namespace Problem
CSS operates in a global namespace, meaning every class name you write is available everywhere in your application. This leads to naming conflicts and makes it difficult to know where styles are being applied.
.button { background: blue; color: white;}
/* component.css */.button { background: red; /* ❌ Conflicts with styles.css */}Specificity Wars
When multiple rules target the same element, CSS specificity determines which styles apply. This often leads to increasingly specific selectors and !important declarations that make styles hard to maintain.
/* ❌ Bad: Escalating specificity */.button {}.button.primary {}.button.primary.large {}.button.primary.large.active {} /* Gets out of hand quickly */Dead Code and Unused Styles
Without proper organization, CSS files accumulate unused styles that increase bundle size and make maintenance difficult. It’s hard to know if a style is still being used without comprehensive testing.
Lack of Encapsulation
Traditional CSS doesn’t provide component-level encapsulation. Styles can leak between components, making it difficult to reason about how changes will affect the application.
These challenges have led to the development of methodologies and tools that provide structure, scoping, and maintainability. Let’s explore the three most popular approaches.
BEM Methodology
BEM (Block Element Modifier) is a naming convention methodology that provides structure and clarity to your CSS class names. It doesn’t require any build tools or frameworks—just a consistent naming pattern.
Understanding BEM Structure
BEM divides components into three parts:
- Block: A standalone, reusable component (e.g.,
button,card,menu) - Element: A part of a block that has no meaning on its own (e.g.,
button__icon,card__title) - Modifier: A variation or state of a block or element (e.g.,
button--primary,button--large)
BEM Naming Convention
The BEM naming pattern follows this structure:
block__element--modifier- Use double underscore (
__) to separate block from element - Use double hyphen (
--) to separate modifier from block or element - Use single hyphen (
-) for multi-word names within blocks, elements, or modifiers
Basic BEM Examples
/* ✅ Block */.button { padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer;}
/* ✅ Element */.button__icon { margin-right: 8px;}
.button__text { font-weight: 600;}
/* ✅ Modifier */.button--primary { background-color: #007bff; color: white;}
.button--secondary { background-color: #6c757d; color: white;}
.button--large { padding: 16px 32px; font-size: 18px;}
/* ✅ Element with modifier */.button__icon--spinning { animation: spin 1s linear infinite;}HTML Structure with BEM
<!-- ✅ Good BEM structure --><button class="button button--primary button--large"> <span class="button__icon button__icon--spinning">⚙️</span> <span class="button__text">Submit</span></button>
<!-- ❌ Bad: Not using BEM --><button class="btn primary large"> <span class="icon spinning">⚙️</span> <span class="text">Submit</span></button>BEM Best Practices
✅ Do:
- Keep blocks independent and reusable
- Use modifiers for variations, not elements
- Nest elements logically within blocks
- Use semantic names that describe purpose, not appearance
/* ✅ Good: Semantic naming */.card {}.card__header {}.card__body {}.card__footer {}.card--featured {}
/* ❌ Bad: Appearance-based naming */.red-box {}.red-box-top {}.red-box-content {}❌ Don’t:
- Create deeply nested elements (avoid
block__element__subelement) - Use BEM classes for styling unrelated elements
- Mix BEM with other naming conventions in the same project
BEM in Modern Frameworks
BEM works well with component-based frameworks. Here’s how it looks in React:
// ✅ Good: BEM with React componentsfunction Button({ variant, size, children, icon }) { const baseClass = "button"; const variantClass = `button--${variant}`; const sizeClass = `button--${size}`;
return ( <button className={`${baseClass} ${variantClass} ${sizeClass}`}> {icon && <span className="button__icon">{icon}</span>} <span className="button__text">{children}</span> </button> );}
// Usage<Button variant="primary" size="large" icon="⚙️"> Submit</Button>;When to Use BEM
BEM is ideal when:
- You want a simple, no-build-tool solution
- Your team needs consistent naming conventions
- You’re working with vanilla CSS or preprocessors (Sass, Less)
- You want to improve code readability and maintainability
- You need a methodology that works across different frameworks
CSS Modules
CSS Modules solve the scoping problem by automatically generating unique class names at build time. Each CSS file becomes a module with locally scoped classes that can’t conflict with classes from other modules.
How CSS Modules Work
CSS Modules transform your class names into unique, hashed identifiers during the build process. This ensures that styles from one component can’t accidentally affect another component.
.button { padding: 12px 24px; background-color: #007bff; color: white; border: none; border-radius: 4px;}
.primary { background-color: #007bff;}
.secondary { background-color: #6c757d;}When imported, CSS Modules generate unique class names:
import styles from "./Button.module.css";
function Button({ variant, children }) { return ( <button className={`${styles.button} ${styles[variant]}`}> {children} </button> );}
// Generated HTML:// <button class="Button_button__a1b2c3 Button_primary__d4e5f6">Setting Up CSS Modules
CSS Modules work out of the box with most modern build tools:
Vite:
export default { css: { modules: { localsConvention: "camelCase", // Allows styles.buttonName }, },};Webpack:
module.exports = { module: { rules: [ { test: /\.module\.css$/, use: [ "style-loader", { loader: "css-loader", options: { modules: { localIdentName: "[name]__[local]__[hash:base64:5]", }, }, }, ], }, ], },};Next.js:
CSS Modules work automatically when you use the .module.css extension.
CSS Modules Features
Composition: CSS Modules support composition, allowing you to combine classes from different modules.
.button { padding: 12px 24px; border: none;}
/* PrimaryButton.module.css */.button { composes: button from "./Button.module.css"; background-color: #007bff; color: white;}Global Styles:
You can still use global styles when needed with :global().
.button { padding: 12px 24px;}
/* ✅ Global class (not scoped) */:global(.global-button) { margin: 10px;}
/* ✅ Global selector */:global(.some-global-class) .button { margin-top: 20px;}Camel Case Conversion: CSS Modules can convert kebab-case class names to camelCase for easier JavaScript usage.
.button-primary {}// ✅ Access as camelCaseimport styles from "./styles.module.css";<div className={styles.buttonPrimary} />;CSS Modules with React
Here’s a complete example of using CSS Modules in a React component:
// Card.module.css.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; background: white; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);}
.header { margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #eee;}
.title { font-size: 24px; font-weight: 600; margin: 0;}
.body { color: #666; line-height: 1.6;}
.footer { margin-top: 16px; padding-top: 12px; border-top: 1px solid #eee;}
.featured { border-color: #007bff; box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2);}import styles from "./Card.module.css";import cn from "classnames"; // Utility for conditional classes
function Card({ title, children, footer, featured }) { return ( <div className={cn(styles.card, { [styles.featured]: featured })}> {title && ( <div className={styles.header}> <h2 className={styles.title}>{title}</h2> </div> )} <div className={styles.body}>{children}</div> {footer && <div className={styles.footer}>{footer}</div>} </div> );}
export default Card;CSS Modules Advantages
✅ Automatic Scoping: No naming conflicts ✅ Build-Time Optimization: Unused styles can be tree-shaken ✅ Type Safety: Works well with TypeScript ✅ No Runtime Overhead: Styles are processed at build time ✅ Familiar CSS Syntax: Use standard CSS without learning new syntax
CSS Modules Limitations
❌ No Dynamic Styles: Can’t generate styles based on props at runtime ❌ Build Tool Required: Needs webpack, Vite, or similar ❌ No Theming Built-in: Requires additional setup for theming ❌ Composition Complexity: Can become complex with many composed modules
Styled Components
Styled Components is a CSS-in-JS library that allows you to write CSS directly in your JavaScript/TypeScript files. It provides component-scoped styling with the ability to use props and JavaScript logic.
What Are Styled Components?
Styled Components uses tagged template literals to create React components with attached styles. Each styled component generates a unique class name, ensuring style encapsulation.
import styled from "styled-components";
// ✅ Create a styled componentconst Button = styled.button` padding: 12px 24px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;
&:hover { background-color: #0056b3; }`;
// Use it like a regular componentfunction App() { return <Button>Click me</Button>;}Installation and Setup
pnpm add styled-componentsFor TypeScript projects:
pnpm add styled-componentspnpm add -D @types/styled-componentsDynamic Styling with Props
One of Styled Components’ biggest advantages is the ability to style based on props:
import styled from 'styled-components';
const Button = styled.button` padding: ${props => props.size === 'large' ? '16px 32px' : '12px 24px'}; font-size: ${props => props.size === 'large' ? '18px' : '16px'}; background-color: ${props => { if (props.variant === 'primary') return '#007bff'; if (props.variant === 'secondary') return '#6c757d'; return '#e9ecef'; }}; color: ${props => props.variant === 'secondary' ? 'white' : '#333'}; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s;
&:hover { background-color: ${props => { if (props.variant === 'primary') return '#0056b3'; if (props.variant === 'secondary') return '#5a6268'; return '#dee2e6'; }}; }
&:disabled { opacity: 0.6; cursor: not-allowed; }`;
// Usage<Button variant="primary" size="large">Submit</Button><Button variant="secondary">Cancel</Button><Button disabled>Disabled</Button>Extending Styled Components
You can extend existing styled components to create variations:
const Button = styled.button` padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer;`;
// ✅ Extend Button to create PrimaryButtonconst PrimaryButton = styled(Button)` background-color: #007bff; color: white;
&:hover { background-color: #0056b3; }`;
// ✅ Extend Button to create LargeButtonconst LargeButton = styled(Button)` padding: 16px 32px; font-size: 18px;`;
// ✅ Combine extensionsconst LargePrimaryButton = styled(PrimaryButton)` padding: 16px 32px; font-size: 18px;`;Theming with Styled Components
Styled Components has built-in theming support through a ThemeProvider:
import styled, { ThemeProvider } from "styled-components";
// Define themeconst theme = { colors: { primary: "#007bff", secondary: "#6c757d", success: "#28a745", danger: "#dc3545", }, spacing: { small: "8px", medium: "16px", large: "24px", }, borderRadius: "4px",};
// Use theme in styled componentsconst Button = styled.button` padding: ${(props) => props.theme.spacing.medium} ${(props) => props.theme.spacing.large}; background-color: ${(props) => props.theme.colors.primary}; border-radius: ${(props) => props.theme.borderRadius}; color: white; border: none; cursor: pointer;`;
// Provide theme to appfunction App() { return ( <ThemeProvider theme={theme}> <Button>Themed Button</Button> </ThemeProvider> );}Advanced Patterns
Conditional Rendering:
const Card = styled.div` border: 1px solid #ddd; border-radius: 8px; padding: 20px; ${(props) => props.featured && ` border-color: #007bff; box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2); `}`;CSS Animations:
import styled, { keyframes } from "styled-components";
const spin = keyframes` from { transform: rotate(0deg); } to { transform: rotate(360deg); }`;
const Spinner = styled.div` border: 4px solid #f3f3f3; border-top: 4px solid #007bff; border-radius: 50%; width: 40px; height: 40px; animation: ${spin} 1s linear infinite;`;Media Queries:
const Container = styled.div` padding: 20px;
@media (max-width: 768px) { padding: 10px; }
@media (min-width: 1200px) { padding: 40px; }`;Pseudo-elements and Pseudo-selectors:
const List = styled.ul` list-style: none; padding: 0;
li { padding: 8px 0; border-bottom: 1px solid #eee;
&:last-child { border-bottom: none; }
&:hover { background-color: #f8f9fa; } }
li::before { content: "✓ "; color: #28a745; margin-right: 8px; }`;Styled Components Best Practices
✅ Do:
- Use semantic component names
- Extract common styles into base components
- Use the theme for consistent design tokens
- Keep styled components close to where they’re used
- Use TypeScript for type safety
// ✅ Good: TypeScript with Styled Componentsimport styled from 'styled-components';
type ButtonProps = { variant?: 'primary' | 'secondary'; size?: 'small' | 'medium' | 'large';};
const Button = styled.button<ButtonProps>` /* styles */`;❌ Don’t:
- Create too many small styled components
- Put all styles in a single file
- Use inline styles when styled components would be better
- Forget to handle responsive design
Performance Considerations
Styled Components processes styles at runtime, which can impact performance. Here are optimization strategies:
Use babel-plugin-styled-components:
{ "plugins": [ [ "babel-plugin-styled-components", { "displayName": true, "fileName": false } ] ]}Avoid creating styled components in render:
// ❌ Bad: Creates new component on every renderfunction Component() { const StyledDiv = styled.div` color: red; `; return <StyledDiv>Content</StyledDiv>;}
// ✅ Good: Define outside componentconst StyledDiv = styled.div` color: red;`;function Component() { return <StyledDiv>Content</StyledDiv>;}Comparing the Approaches
Each CSS architecture approach has its strengths and weaknesses. Here’s a comprehensive comparison:
| Feature | BEM | CSS Modules | Styled Components |
|---|---|---|---|
| Scoping | Manual (naming) | Automatic (build-time) | Automatic (runtime) |
| Build Tools Required | No | Yes | Yes |
| Dynamic Styles | Limited | No | Yes (props-based) |
| Learning Curve | Low | Medium | Medium-High |
| Bundle Size | Small | Small | Larger (runtime) |
| TypeScript Support | Manual | Good | Excellent |
| Theming | Manual | Manual | Built-in |
| Performance | Excellent | Excellent | Good (with optimization) |
| Framework Agnostic | Yes | Mostly (needs build tool) | React only |
| Code Splitting | Manual | Automatic | Automatic |
Use Case Scenarios
Choose BEM when:
- You want a simple, no-dependency solution
- Working with vanilla JavaScript or multiple frameworks
- Team needs consistent naming conventions
- Performance is critical (no runtime overhead)
- You prefer traditional CSS workflow
Choose CSS Modules when:
- You need automatic scoping without runtime overhead
- Working with React, Vue, or other component frameworks
- You want build-time optimization
- You prefer CSS syntax over CSS-in-JS
- TypeScript integration is important
Choose Styled Components when:
- You need dynamic, prop-based styling
- Working exclusively with React
- Theming is a core requirement
- You want component-scoped styles with JavaScript logic
- Developer experience and co-location are priorities
Choosing the Right Approach
The best CSS architecture depends on your project’s specific needs. Consider these factors:
Project Size and Complexity
Small Projects:
- BEM or CSS Modules work well
- Styled Components might be overkill
Medium Projects:
- CSS Modules or Styled Components
- BEM if you prefer simplicity
Large Projects:
- CSS Modules for performance-critical apps
- Styled Components for complex theming needs
- BEM as a foundation with other tools
Team Preferences and Skills
Consider your team’s experience:
- CSS-focused developers: BEM or CSS Modules
- JavaScript-focused developers: Styled Components
- Mixed teams: CSS Modules (familiar CSS syntax)
Performance Requirements
For performance-critical applications:
- CSS Modules - Best performance (build-time)
- BEM - Excellent performance (no runtime)
- Styled Components - Good performance (with optimization)
Framework and Tooling
- React only: All three work, Styled Components is most popular
- Vue: CSS Modules or BEM
- Vanilla JS: BEM
- Multiple frameworks: BEM for consistency
Migration Path
Consider your current setup:
- Existing CSS: BEM is easiest to adopt
- React app: CSS Modules or Styled Components
- New project: Choose based on requirements
Migration Strategies
If you’re migrating from one approach to another, here are practical strategies:
Migrating to BEM
Step 1: Audit existing styles
/* Before */.btn {}.btn-primary {}.btn-large {}
/* After */.button {}.button--primary {}.button--large {}Step 2: Create naming convention guide Document your BEM patterns and share with the team.
Step 3: Migrate incrementally Start with new components, gradually refactor existing ones.
Migrating to CSS Modules
Step 1: Rename files
styles.css → Component.module.cssStep 2: Update imports
// Beforeimport "./styles.css";
// Afterimport styles from "./Component.module.css";Step 3: Update class names
// Before<div className="card">
// After<div className={styles.card}>Step 4: Handle global styles
Move truly global styles to a separate file without .module.css extension.
Migrating to Styled Components
Step 1: Install dependencies
pnpm add styled-componentsStep 2: Convert components incrementally
// Beforeimport "./Button.css";function Button({ variant, children }) { return <button className={`button button--${variant}`}>{children}</button>;}
// Afterimport styled from "styled-components";const Button = styled.button` /* styles */`;Step 3: Set up theming
Create a theme and wrap your app with ThemeProvider.
Step 4: Remove CSS files Once components are migrated, remove old CSS files.
Hybrid Approaches
You can combine approaches:
BEM + CSS Modules:
.block {}.block__element {}.block--modifier {}CSS Modules + Styled Components: Use CSS Modules for static styles, Styled Components for dynamic styles.
Best Practices and Common Pitfalls
General Best Practices
✅ Consistent Naming: Use consistent naming conventions throughout your project. Whether you choose BEM, CSS Modules, or Styled Components, stick to your chosen approach.
✅ Component Co-location: Keep styles close to components. For CSS Modules and Styled Components, co-locate styles with components.
✅ Design System: Establish design tokens (colors, spacing, typography) early. Use CSS custom properties, theme objects, or variables.
✅ Performance:
- Minimize CSS bundle size
- Use code splitting
- Remove unused styles
- Optimize critical CSS
✅ Accessibility: Don’t forget accessibility when styling. Ensure sufficient color contrast, focus states, and responsive design.
Common Pitfalls
❌ Over-nesting (BEM):
/* ❌ Bad: Too deep */.card__header__title__icon {}
/* ✅ Good: Flatten structure */.card__header-title-icon {}❌ Global Styles Leakage (CSS Modules):
/* ❌ Bad: Accidentally global */.button {} /* Should be scoped */
/* ✅ Good: Explicit global when needed */:global(.button) {}❌ Runtime Style Creation (Styled Components):
// ❌ Bad: Creates new component every renderfunction Component() { const Styled = styled.div` color: red; `; return <Styled />;}
// ✅ Good: Define outside componentconst Styled = styled.div` color: red;`;function Component() { return <Styled />;}❌ Inconsistent Spacing: Use a consistent spacing scale (4px, 8px, 16px, etc.) instead of arbitrary values.
❌ Magic Numbers: Avoid hardcoded values. Use variables, theme values, or constants.
/* ❌ Bad */.element { margin-top: 23px; /* Why 23? */}
/* ✅ Good */.element { margin-top: var(--spacing-large);}Testing Styles
Regardless of your chosen approach, test your styles:
Visual Regression Testing: Use tools like Chromatic, Percy, or Storybook to catch visual regressions.
Responsive Testing: Test on multiple screen sizes and devices.
Browser Testing: Ensure styles work across different browsers.
Accessibility Testing: Use tools like axe or Lighthouse to check accessibility.
Conclusion
CSS architecture is fundamental to building maintainable, scalable frontend applications. BEM provides structure through naming conventions, CSS Modules offer automatic scoping at build time, and Styled Components enable dynamic, component-scoped styling in React applications.
Each approach has its place:
- BEM excels in simplicity and framework-agnostic projects
- CSS Modules provide the best balance of scoping and performance
- Styled Components offer the most flexibility for dynamic styling and theming
The key is choosing the right approach for your specific project needs, team skills, and performance requirements. You can even combine approaches—using BEM naming with CSS Modules, or CSS Modules for static styles with Styled Components for dynamic ones.
Remember that good CSS architecture is about more than just the tool or methodology you choose. It’s about consistency, maintainability, and creating a system that scales with your application. Start with one approach, establish patterns and conventions, and iterate based on your team’s needs.
For more CSS tips and techniques, check out our TailwindCSS cheatsheet or explore our guide on web performance optimization to see how CSS architecture impacts site speed.
Whether you’re starting a new project or refactoring an existing one, investing time in CSS architecture will pay dividends in maintainability, developer experience, and application performance.