Back to Blog
CSSArchitectureTailwindBest Practices

CSS Architecture Patterns for Scalable Stylesheets

Organize CSS that scales. From BEM to CSS Modules to utility-first approaches for maintainable styles.

B
Bootspring Team
Engineering
August 5, 2024
4 min read

CSS at scale requires organization. Without structure, stylesheets become unmaintainable. Here are patterns that keep CSS manageable as projects grow.

The Specificity Problem#

1/* Specificity wars */ 2.button { color: blue; } 3.header .button { color: red; } 4.header .nav .button { color: green; } 5#main .header .nav .button { color: purple; } 6.button { color: orange !important; } /* Desperation */

BEM (Block Element Modifier)#

1/* Block: Standalone component */ 2.card { } 3 4/* Element: Part of block (double underscore) */ 5.card__header { } 6.card__body { } 7.card__footer { } 8 9/* Modifier: Variation (double hyphen) */ 10.card--featured { } 11.card--compact { } 12.card__header--large { }
1<div class="card card--featured"> 2 <div class="card__header card__header--large"> 3 <h2 class="card__title">Title</h2> 4 </div> 5 <div class="card__body">Content</div> 6 <div class="card__footer"> 7 <button class="card__button">Action</button> 8 </div> 9</div>

BEM Benefits#

1/* Flat specificity - all selectors are equal */ 2.card { } 3.card--featured { } 4.card__header { } 5 6/* Easy to understand scope */ 7/* .card__header belongs to .card */ 8 9/* Safe to modify - no cascade surprises */

CSS Modules#

1/* Button.module.css */ 2.button { 3 padding: 8px 16px; 4 border-radius: 4px; 5} 6 7.primary { 8 background: blue; 9 color: white; 10} 11 12.secondary { 13 background: gray; 14 color: black; 15}
1// Button.tsx 2import styles from './Button.module.css'; 3 4function Button({ variant = 'primary', children }) { 5 return ( 6 <button className={`${styles.button} ${styles[variant]}`}> 7 {children} 8 </button> 9 ); 10} 11 12// Compiled output: 13// <button class="Button_button_x7d2s Button_primary_a3f1k">

CSS Modules Benefits#

/* Automatic unique class names */ /* No naming collisions */ /* Local scope by default */ /* Dead code elimination possible */

Utility-First (Tailwind CSS)#

1<!-- Traditional CSS --> 2<button class="btn btn-primary btn-large">Submit</button> 3 4<!-- Utility-first --> 5<button class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> 6 Submit 7</button>

Component Extraction#

1// When utilities repeat, extract components 2function Button({ variant = 'primary', size = 'md', children }) { 3 const baseStyles = 'font-medium rounded-lg transition-colors'; 4 5 const variants = { 6 primary: 'bg-blue-600 text-white hover:bg-blue-700', 7 secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300', 8 }; 9 10 const sizes = { 11 sm: 'px-3 py-1.5 text-sm', 12 md: 'px-4 py-2', 13 lg: 'px-6 py-3 text-lg', 14 }; 15 16 return ( 17 <button className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}> 18 {children} 19 </button> 20 ); 21}

@apply for Reuse#

1/* styles.css */ 2@layer components { 3 .btn { 4 @apply font-medium rounded-lg transition-colors; 5 } 6 7 .btn-primary { 8 @apply bg-blue-600 text-white hover:bg-blue-700; 9 } 10 11 .btn-secondary { 12 @apply bg-gray-200 text-gray-800 hover:bg-gray-300; 13 } 14}

CSS-in-JS#

1// Styled Components 2import styled from 'styled-components'; 3 4const Button = styled.button<{ $primary?: boolean }>` 5 padding: 8px 16px; 6 border-radius: 4px; 7 background: ${props => props.$primary ? 'blue' : 'gray'}; 8 color: ${props => props.$primary ? 'white' : 'black'}; 9 10 &:hover { 11 opacity: 0.9; 12 } 13`; 14 15// Usage 16<Button $primary>Submit</Button>
1// Emotion 2import { css } from '@emotion/react'; 3 4const buttonStyle = css` 5 padding: 8px 16px; 6 border-radius: 4px; 7`; 8 9function Button({ children }) { 10 return <button css={buttonStyle}>{children}</button>; 11}

File Organization#

Feature-Based#

src/ ├── components/ │ ├── Button/ │ │ ├── Button.tsx │ │ ├── Button.module.css │ │ └── Button.test.tsx │ └── Card/ │ ├── Card.tsx │ └── Card.module.css ├── styles/ │ ├── globals.css │ ├── variables.css │ └── reset.css

Layer-Based (ITCSS)#

styles/ ├── 01-settings/ # Variables, config │ └── _variables.css ├── 02-tools/ # Mixins, functions │ └── _mixins.css ├── 03-generic/ # Reset, normalize │ └── _reset.css ├── 04-elements/ # Bare HTML elements │ └── _typography.css ├── 05-objects/ # Layout patterns │ └── _grid.css ├── 06-components/ # UI components │ └── _button.css └── 07-utilities/ # Helper classes └── _utilities.css

CSS Custom Properties#

1:root { 2 /* Colors */ 3 --color-primary: #3b82f6; 4 --color-primary-dark: #2563eb; 5 6 /* Spacing */ 7 --spacing-sm: 0.5rem; 8 --spacing-md: 1rem; 9 --spacing-lg: 2rem; 10 11 /* Typography */ 12 --font-sans: 'Inter', sans-serif; 13 --font-size-base: 1rem; 14 15 /* Shadows */ 16 --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); 17 --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); 18} 19 20.button { 21 padding: var(--spacing-sm) var(--spacing-md); 22 background: var(--color-primary); 23 font-family: var(--font-sans); 24} 25 26.button:hover { 27 background: var(--color-primary-dark); 28} 29 30/* Theme switching */ 31[data-theme="dark"] { 32 --color-primary: #60a5fa; 33 --color-primary-dark: #3b82f6; 34}

Best Practices#

1/* 1. Avoid deep nesting */ 2/* ❌ Bad */ 3.page .content .sidebar .widget .button { } 4 5/* ✅ Good */ 6.sidebar-widget__button { } 7 8/* 2. Avoid !important */ 9/* If needed, it indicates a specificity problem */ 10 11/* 3. Keep selectors simple */ 12/* ❌ Bad */ 13div.container > ul.nav li a.active { } 14 15/* ✅ Good */ 16.nav__link--active { } 17 18/* 4. Use logical properties */ 19/* Instead of */ 20margin-left: 1rem; 21/* Use */ 22margin-inline-start: 1rem;

Conclusion#

Choose an architecture that fits your team and project. BEM works great for traditional CSS, CSS Modules provide scoping, utility-first excels for rapid development, and CSS-in-JS offers full JavaScript integration.

The best CSS architecture is one your team follows consistently.

Share this article

Help spread the word about Bootspring