DevToolBoxGRATIS
Blogg

CSS Architecture Guide: BEM, CSS Modules, CSS-in-JS, Tailwind, Custom Properties, and Dark Mode

16 min readby DevToolBox

TL;DR

Modern CSS architecture is about scoping, predictability, and maintainability at scale. Choose BEM for global CSS in multi-developer teams, CSS Modules for zero-runtime scoped styles in React/Next.js, CSS-in-JS when styles depend on runtime props, and Tailwind for utility-first rapid development. Use CSS custom properties for live theming and dark mode, @layer to manage specificity wars, Grid for 2D layouts, and Flexbox for 1D alignment. Animate only transform and opacity for 60 fps performance.

Key Takeaways

  • BEM = block__element--modifier — prevents naming collisions without build tooling
  • CSS Modules auto-scope class names at build time with zero runtime cost
  • CSS-in-JS enables prop-driven dynamic styles; prefer vanilla-extract for zero-runtime
  • Tailwind removes naming decisions but requires component extraction to avoid class soup
  • CSS custom properties cascade and update at runtime — the foundation of live theming
  • Use Grid for page-level 2D layouts, Flexbox for component-level 1D alignment
  • @layer makes specificity predictable; layers defined later always win
  • Only animate transform and opacity for GPU-composited 60 fps animations
  • Container queries replace many breakpoint patterns for truly reusable components
  • Dark mode: CSS variables on :root + prefers-color-scheme + [data-theme] override

CSS Methodologies: BEM, SMACSS, and OOCSS

Before CSS Modules and CSS-in-JS existed, the web community developed naming conventions and architecture methodologies to tame global CSS at scale. These methodologies are still widely used in projects that rely on plain CSS or Sass without component-scoping build tools.

BEM (Block Element Modifier)

BEM structures class names as block__element--modifier. A block is a standalone, reusable component. An element is a part of a block that has no standalone meaning. A modifier is a flag on a block or element that changes its appearance or behavior.

/* Block: the component itself */
.card { ... }

/* Elements: parts of the block */
.card__title { ... }
.card__body { ... }
.card__footer { ... }
.card__image { ... }

/* Modifiers: state/variant flags */
.card--featured { ... }          /* block modifier */
.card--size-large { ... }        /* block modifier */
.card__title--truncated { ... }  /* element modifier */

/* BEM in HTML */
<article class="card card--featured">
  <img class="card__image" src="..." alt="..." />
  <h2 class="card__title card__title--truncated">...</h2>
  <div class="card__body">...</div>
  <footer class="card__footer">...</footer>
</article>

BEM's double-underscore and double-hyphen syntax looks verbose, but it makes the relationship between classes immediately clear without reading the HTML structure. It completely eliminates specificity wars because you almost never need to nest selectors.

SMACSS (Scalable and Modular Architecture for CSS)

SMACSS, created by Jonathan Snook, categorizes CSS rules into five types: Base (element defaults), Layout (page structure), Module (reusable components), State (JavaScript-driven states), and Theme (visual theming overrides).

/* Base rules — element defaults, no classes */
body { margin: 0; font-family: sans-serif; }
a { color: inherit; }

/* Layout rules — l- prefix */
.l-header { ... }
.l-sidebar { ... }
.l-main { ... }

/* Module rules — the component */
.nav { ... }
.nav-item { ... }

/* State rules — is- prefix, often applied via JS */
.is-active { ... }
.is-hidden { display: none; }
.is-loading { opacity: 0.5; }

/* Theme rules — override module styles */
.theme-ocean .nav { background: #0284c7; }
.theme-forest .nav { background: #15803d; }

OOCSS (Object-Oriented CSS)

OOCSS, introduced by Nicole Sullivan, has two core principles: separate structure from skin and separate container from content. Structure defines layout properties (width, height, padding, margin) while skin defines visual properties (background, border, color). This produces small, combinable classes instead of monolithic component classes.

/* Structure class — defines layout */
.media { display: flex; align-items: flex-start; gap: 16px; }
.media__body { flex: 1; }

/* Skin classes — define appearance */
.box-shadow { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.rounded { border-radius: 8px; }
.bg-white { background: #fff; }

/* Combine in HTML — similar to Tailwind's philosophy */
<div class="media box-shadow rounded bg-white">
  <img class="media__image" src="..." />
  <div class="media__body">...</div>
</div>

/* Avoid: container-dependent styles */
/* Bad — only works inside .sidebar */
.sidebar h3 { font-size: 14px; }
/* Good — reusable */
.widget-title { font-size: 14px; }

CSS Modules: Scoped Styles in React and Next.js

CSS Modules solve the global namespace problem at the build level, not through naming conventions. Every class name in a .module.css file is locally scoped by default. The build tool (webpack, Vite, Turbopack) replaces class names with unique hashes, preventing any collision even if two components define a class called .title.

/* Card.module.css */
.card {
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  background: var(--bg-surface);
}

.title {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--text-primary);
  margin: 0 0 0.5rem;
}

.body {
  color: var(--text-secondary);
  line-height: 1.6;
}

/* Modifiers via composes or conditional className */
.featured {
  composes: card;           /* inherit all card styles */
  border: 2px solid var(--accent);
}
// Card.tsx — React component with CSS Modules
import styles from './Card.module.css';

interface CardProps {
  title: string;
  children: React.ReactNode;
  featured?: boolean;
}

export function Card({ title, children, featured }: CardProps) {
  return (
    <article className={featured ? styles.featured : styles.card}>
      <h2 className={styles.title}>{title}</h2>
      <div className={styles.body}>{children}</div>
    </article>
  );
}

// In the browser DOM, the class becomes something like:
// class="Card_card__a3f8x"  or  "Card_featured__b9d2e"

CSS Modules with clsx for Conditional Classes

import clsx from 'clsx';
import styles from './Button.module.css';

type ButtonProps = {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  children: React.ReactNode;
  onClick?: () => void;
};

export function Button({
  variant = 'primary',
  size = 'md',
  disabled,
  children,
  onClick,
}: ButtonProps) {
  return (
    <button
      className={clsx(
        styles.button,
        styles['variant-' + variant],
        styles['size-' + size],
        disabled && styles.disabled
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

Global Styles Within CSS Modules

CSS Modules scopes everything by default. When you need to target a global class (e.g., from a third-party library), use the :global() pseudo-class.

/* Target a global class from within a CSS Module */
.wrapper :global(.ql-editor) {
  font-size: 1rem;
  line-height: 1.7;
}

/* Conversely, define a globally-scoped class from a module */
:global(.sr-only) {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
}

CSS-in-JS: styled-components, emotion, and vanilla-extract

CSS-in-JS libraries colocate styles with component logic, enabling prop-driven dynamic styling, automatic critical CSS extraction, and type-safe theming. The trade-off is runtime overhead (for client-side injection libraries) and increased bundle size.

styled-components

import styled, { css } from 'styled-components';

// Basic styled component
const Card = styled.article`
  border-radius: 8px;
  padding: 20px;
  background: ${({ theme }) => theme.colors.surface};
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
`;

// Conditional styles from props
const Button = styled.button<{ variant: 'primary' | 'secondary'; size?: 'sm' | 'lg' }>`
  padding: ${({ size }) => size === 'sm' ? '6px 12px' : size === 'lg' ? '14px 28px' : '10px 20px'};
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
  border: none;

  ${({ variant, theme }) =>
    variant === 'primary'
      ? css`
          background: ${theme.colors.primary};
          color: #fff;
          &:hover { background: ${theme.colors.primaryHover}; }
        `
      : css`
          background: transparent;
          color: ${theme.colors.primary};
          border: 2px solid ${theme.colors.primary};
          &:hover { background: ${theme.colors.primarySubtle}; }
        `}
`;

// Theme provider setup
import { ThemeProvider } from 'styled-components';

const theme = {
  colors: {
    primary: '#3b82f6',
    primaryHover: '#2563eb',
    primarySubtle: '#eff6ff',
    surface: '#ffffff',
  },
};

export function App() {
  return (
    <ThemeProvider theme={theme}>
      <Card>
        <Button variant="primary" size="lg">Get Started</Button>
        <Button variant="secondary">Learn More</Button>
      </Card>
    </ThemeProvider>
  );
}

emotion

emotion offers both a styled API similar to styled-components and a css prop that can be added to any JSX element, giving more granular control without creating named components.

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

const cardStyle = css`
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
`;

const titleStyle = css`
  font-size: 1.25rem;
  font-weight: 700;
  margin: 0 0 0.5rem;
`;

// Use the css prop directly on JSX elements
export function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <article css={cardStyle}>
      <h2 css={titleStyle}>{title}</h2>
      <div>{children}</div>
    </article>
  );
}

// Dynamic styles with cx (class composition)
import { cx } from '@emotion/css';

const base = css`color: #334155;`;
const active = css`color: #3b82f6; font-weight: 600;`;

<span className={cx(base, isActive && active)}>Item</span>

vanilla-extract: Zero-Runtime CSS-in-JS

vanilla-extract writes styles in TypeScript .css.ts files that are statically analyzed at build time and converted to real CSS classes — no runtime overhead. It combines the type-safety of CSS-in-JS with the performance of CSS Modules.

// button.css.ts — statically analyzed at build time
import { style, styleVariants } from '@vanilla-extract/css';

export const base = style({
  borderRadius: '6px',
  fontWeight: 600,
  cursor: 'pointer',
  border: 'none',
  padding: '10px 20px',
  transition: 'background 0.15s ease',
});

export const variants = styleVariants({
  primary: {
    background: '#3b82f6',
    color: '#fff',
    ':hover': { background: '#2563eb' },
  },
  secondary: {
    background: 'transparent',
    color: '#3b82f6',
    border: '2px solid #3b82f6',
    ':hover': { background: '#eff6ff' },
  },
  ghost: {
    background: 'transparent',
    color: '#64748b',
    ':hover': { background: '#f1f5f9' },
  },
});

// button.tsx — no runtime CSS injection
import { base, variants } from './button.css';

export function Button({ variant = 'primary', children }: ButtonProps) {
  return <button className={base + ' ' + variants[variant]}>{children}</button>;
}

Tailwind CSS: Utility-First Approach

Tailwind CSS provides a comprehensive set of single-purpose utility classes. Instead of writing custom CSS, you compose styles directly in the HTML/JSX using classes like flex items-center gap-4 rounded-lg bg-blue-500 px-4 py-2 text-white. Tailwind's build process (PurgeCSS / content scanning) removes all unused classes, resulting in very small production CSS files.

Basic Tailwind Patterns

{/* Card component using Tailwind */}
<article className="rounded-xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
  <h2 className="mb-2 text-xl font-bold text-slate-900">Card Title</h2>
  <p className="text-slate-600 leading-relaxed">Card body content goes here.</p>
  <footer className="mt-4 flex items-center justify-between">
    <span className="text-sm text-slate-400">By Author</span>
    <a className="text-sm font-semibold text-blue-600 hover:text-blue-700">Read more</a>
  </footer>
</article>

{/* Responsive variants */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
  {items.map(item => <Card key={item.id} {...item} />)}
</div>

{/* State variants: hover, focus, active, dark */}
<button className="
  bg-blue-600 text-white px-4 py-2 rounded-lg
  hover:bg-blue-700
  focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
  active:scale-95
  dark:bg-blue-500 dark:hover:bg-blue-600
  transition-all duration-150
">
  Submit
</button>

Tailwind Configuration and Design Tokens

// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: ['./src/**/*.{ts,tsx}'],
  darkMode: 'class',  // or 'media'
  theme: {
    extend: {
      colors: {
        brand: {
          50:  '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        },
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
        mono: ['Fira Code', 'monospace'],
      },
      spacing: {
        18: '4.5rem',
        88: '22rem',
      },
      borderRadius: {
        '4xl': '2rem',
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
  ],
};

export default config;

Avoiding Class Soup with Component Extraction

// Instead of repeating long class strings everywhere:
// <button class="bg-blue-600 text-white px-4 py-2 rounded-lg font-semibold ...">

// Extract to a variant-aware component using cva (class-variance-authority)
import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva(
  // Base classes always applied
  'inline-flex items-center justify-center rounded-lg font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
        secondary: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-500',
        ghost: 'text-slate-600 hover:bg-slate-100 focus:ring-slate-400',
        destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-base',
        lg: 'px-6 py-3 text-lg',
      },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  }
);

type ButtonProps = VariantProps<typeof buttonVariants> & React.ButtonHTMLAttributes<HTMLButtonElement>;

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return <button className={buttonVariants({ variant, size, className })} {...props} />;
}

CSS Custom Properties for Theming

CSS custom properties (variables) are native CSS features that cascade through the DOM, can be updated at runtime with JavaScript, and enable component-level theming through inherited values. They are the foundation of any robust design token system.

/* Design token system with CSS custom properties */
:root {
  /* Primitive tokens — raw values */
  --color-blue-50:  #eff6ff;
  --color-blue-500: #3b82f6;
  --color-blue-600: #2563eb;
  --color-slate-50: #f8fafc;
  --color-slate-900: #0f172a;

  /* Semantic tokens — reference primitives */
  --bg-primary:   var(--color-slate-50);
  --bg-surface:   #ffffff;
  --text-primary: var(--color-slate-900);
  --text-muted:   #64748b;
  --accent:       var(--color-blue-500);
  --accent-hover: var(--color-blue-600);

  /* Spacing scale */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  /* Typography */
  --font-sans: 'Inter', system-ui, sans-serif;
  --font-size-sm:   0.875rem;
  --font-size-base: 1rem;
  --font-size-lg:   1.125rem;
  --font-size-xl:   1.25rem;

  /* Radii */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.1);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.15);
}

/* Components reference semantic tokens, not primitive values */
.card {
  background: var(--bg-surface);
  color: var(--text-primary);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  box-shadow: var(--shadow-md);
}

.button-primary {
  background: var(--accent);
  color: #fff;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
}

.button-primary:hover {
  background: var(--accent-hover);
}

Updating Variables at Runtime with JavaScript

// Read a CSS variable
const accent = getComputedStyle(document.documentElement)
  .getPropertyValue('--accent').trim();

// Write a CSS variable — all components using var(--accent) update instantly
document.documentElement.style.setProperty('--accent', '#8b5cf6');

// Brand color switcher
function setBrandColor(hue: number) {
  const root = document.documentElement;
  root.style.setProperty('--accent',       `hsl(${hue}, 82%, 55%)`);
  root.style.setProperty('--accent-hover', `hsl(${hue}, 82%, 45%)`);
  root.style.setProperty('--accent-subtle',`hsl(${hue}, 82%, 97%)`);
}

// Apply to scoped element (not just :root)
const card = document.querySelector('.card') as HTMLElement;
card.style.setProperty('--card-bg', '#fefce8'); // only affects this card

CSS Grid vs Flexbox: When to Use Which

Grid and Flexbox are both CSS layout systems, but they solve different problems. Knowing when to reach for each — and when to combine them — is a key skill for modern CSS architecture.

AspectCSS GridFlexbox
Dimensions2D (rows + columns)1D (row OR column)
Primary usePage layouts, card grids, dashboardsComponent-level alignment, navbars, button groups
PlacementPlace items at specific grid lines/areasItems flow in sequence
OverlapSupported (grid-area, z-index)Requires positioning hacks
Alignmentalign-items, justify-items, place-itemsalign-items, justify-content, gap
Responsiveauto-fit + minmax — no media queriesflex-wrap + flex-grow/shrink
Content-drivenExplicit rows/cols defined by authorItems dictate their own size

Grid for Page Layout, Flexbox for Components

/* Grid: macro page layout */
.app-layout {
  display: grid;
  grid-template-areas:
    "header  header"
    "sidebar main"
    "footer  footer";
  grid-template-columns: 240px 1fr;
  grid-template-rows: 64px 1fr 48px;
  min-height: 100vh;
}

.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

/* Responsive card grid — no media query needed */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 24px;
}

/* Flexbox: micro component layout */
.nav {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 24px;
}

.nav__logo { margin-right: auto; } /* push everything else right */

.button-group {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* Centering — the #1 use case for Flexbox */
.center-both {
  display: flex;
  align-items: center;
  justify-content: center;
}

Responsive Design: Mobile-First and Container Queries

Mobile-first responsive design means writing base styles for small screens and adding complexity for larger screens via min-width media queries. Container queries (2023) go further — components respond to their container's size, not the viewport, enabling truly reusable components.

Mobile-First Media Queries

/* Mobile-first breakpoint system */
/* Tailwind's default breakpoints as reference */
/* sm:  640px  — small devices, large phones */
/* md:  768px  — tablets */
/* lg:  1024px — small laptops */
/* xl:  1280px — desktops */
/* 2xl: 1536px — large screens */

/* Card component — mobile-first */
.card {
  padding: 16px;          /* mobile: compact */
  font-size: 0.875rem;    /* mobile: smaller text */
}

@media (min-width: 768px) {
  .card {
    padding: 24px;        /* tablet+: more breathing room */
    font-size: 1rem;
  }
}

@media (min-width: 1024px) {
  .card {
    padding: 32px;        /* desktop: generous padding */
  }
}

/* Prefer min-width (mobile-first) over max-width (desktop-first)
   because it's easier to add complexity than remove it */

Container Queries

Container queries allow a component to respond to its parent container's width, not the viewport width. This solves the classic problem where the same Card component needs to look different when placed in a narrow sidebar vs. a wide main column.

/* Step 1: Define the containment context on the parent */
.card-wrapper {
  container-type: inline-size;
  container-name: card;   /* optional: named container */
}

/* Step 2: Query the container size in the child */
.card {
  display: block;         /* stacked layout for narrow containers */
}

.card__image { width: 100%; }

@container card (min-width: 400px) {
  /* Side-by-side layout when container >= 400px */
  .card {
    display: grid;
    grid-template-columns: 160px 1fr;
    gap: 16px;
    align-items: start;
  }
}

@container card (min-width: 600px) {
  /* Larger image and bigger text for wide containers */
  .card {
    grid-template-columns: 240px 1fr;
  }
  .card__title { font-size: 1.5rem; }
}

/* This same Card component now works correctly in
   both a narrow sidebar (stacked) and a wide main
   content area (side-by-side) WITHOUT any props! */

CSS Animations and Performance

CSS animations can run on the main thread (causing layout and paint) or on the compositor thread (GPU-accelerated, no layout/paint). Understanding the rendering pipeline is critical for 60 fps animations.

The Rendering Pipeline

CSS Property Cost Tiers:

  • Free (compositor thread): transform, opacity, filter
  • Paint only: color, background-color, box-shadow, border-color
  • Layout + Paint (expensive): width, height, top, left, margin, padding, font-size
/* GPU-composited animation — 60 fps on any device */
@keyframes slide-in {
  from { transform: translateX(-100%); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

.drawer {
  animation: slide-in 300ms cubic-bezier(0.4, 0, 0.2, 1) both;
}

/* Expensive animation — causes layout on every frame */
@keyframes expand-bad {
  from { width: 0; }
  to   { width: 300px; }
}

/* Better: simulate width change with transform */
@keyframes expand-good {
  from { transform: scaleX(0); transform-origin: left; }
  to   { transform: scaleX(1); transform-origin: left; }
}

/* will-change: promote to its own layer before animation */
.animated-card {
  will-change: transform;  /* hint browser to create GPU layer */
}

/* Remove will-change after animation to free GPU memory */
.animated-card.done {
  will-change: auto;
}

/* Respect prefers-reduced-motion for accessibility */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Keyframe Animation Patterns

/* Fade in */
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Scale in from center */
@keyframes scale-in {
  from { transform: scale(0.9); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}

/* Shake (for error feedback) */
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%       { transform: translateX(-10px); }
  40%       { transform: translateX(10px); }
  60%       { transform: translateX(-6px); }
  80%       { transform: translateX(6px); }
}

/* Pulse loading skeleton */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.5; }
}

.skeleton {
  background: #e2e8f0;
  border-radius: 4px;
  animation: pulse 1.5s ease-in-out infinite;
}

/* Spin for loading indicators */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  animation: spin 0.8s linear infinite;
  will-change: transform;
}

/* Staggered children animations */
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 50ms; }
.list-item:nth-child(3) { animation-delay: 100ms; }
.list-item:nth-child(4) { animation-delay: 150ms; }

CSS Cascade Layers (@layer)

Cascade layers, standardized in 2022 and supported in all modern browsers, allow you to define explicit ordering for CSS rule priority. Styles in a layer declared later always override styles in earlier layers, regardless of specificity. This eliminates specificity wars when integrating third-party styles with your own.

/* Declare layer order first — later layers win */
@layer reset, base, third-party, components, utilities;

/* Fill each layer */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
  img { max-width: 100%; }
}

@layer base {
  body {
    font-family: var(--font-sans);
    color: var(--text-primary);
    background: var(--bg-primary);
  }
  h1, h2, h3 { line-height: 1.25; }
}

@layer third-party {
  /* Import a UI library — its styles stay here */
  @import url('some-ui-library.css');
}

@layer components {
  .card {
    border-radius: 8px;
    padding: 20px;
    /* This wins over third-party .card because components > third-party */
  }
}

@layer utilities {
  /* Always-win utilities */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
  }
  .truncate  { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .visually-hidden { visibility: hidden; }
}

Layers vs !important

/* !important within a layer is still bounded by the layer order */
/* !important reverses the layer order for the cascade */

@layer base, components;

@layer base {
  .title { color: red !important; }   /* !important within base layer */
}

@layer components {
  .title { color: blue; }  /* no !important */
}

/* Result: blue — components layer wins over base layer
   because !important reverses priority between layers,
   making unlayered !important > higher layers' !important > lower layers' !important */

/* Practical rule: prefer proper layer ordering over !important */

PostCSS and CSS Preprocessing

PostCSS is a tool for transforming CSS with JavaScript plugins. Unlike Sass or Less (which are separate languages that compile to CSS), PostCSS processes valid CSS and applies targeted transformations. Most modern build tools (Vite, Next.js, webpack) use PostCSS under the hood.

PostCSS Configuration

// postcss.config.js
module.exports = {
  plugins: [
    require('tailwindcss'),            // Tailwind CSS
    require('autoprefixer'),           // Add vendor prefixes automatically
    require('postcss-nesting'),        // Enable CSS nesting (spec-compliant)
    require('postcss-custom-media'),   // Custom media query names
    require('cssnano')({               // Minify in production
      preset: 'default',
    }),
  ],
};

Sass / SCSS Still Relevant

Despite CSS Variables and native nesting, Sass/SCSS remains widely used for its mixins, functions, loops, and module system. It is especially valuable in large design systems that predate CSS-in-JS adoption.

// _tokens.scss — Sass module
$colors: (
  "blue-50":  #eff6ff,
  "blue-500": #3b82f6,
  "blue-600": #2563eb,
);

// _mixins.scss
@mixin respond-to($bp) {
  $breakpoints: ("sm": 640px, "md": 768px, "lg": 1024px, "xl": 1280px);
  @media (min-width: map-get($breakpoints, $bp)) { @content; }
}

@mixin visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
}

// _card.scss
@use 'tokens' as t;
@use 'mixins' as m;

.card {
  padding: 16px;
  background: white;
  border-radius: 8px;

  @include m.respond-to('md') {
    padding: 24px;
  }

  &__title {
    font-size: 1.25rem;
    font-weight: 700;
  }

  &--featured {
    border: 2px solid map-get(t.$colors, "blue-500");
  }
}

Dark Mode with prefers-color-scheme and CSS Variables

The recommended modern approach to dark mode uses CSS custom properties as semantic tokens combined with the prefers-color-scheme media query. A secondary[data-theme] attribute on <html> enables user-toggled dark mode that overrides the OS preference.

/* Complete dark mode token system */
:root {
  color-scheme: light dark;  /* browser chrome follows suit */

  /* Light mode tokens */
  --bg-base:     #f8fafc;
  --bg-surface:  #ffffff;
  --bg-elevated: #ffffff;
  --text-primary:   #0f172a;
  --text-secondary: #475569;
  --text-muted:     #94a3b8;
  --border:         #e2e8f0;
  --border-focus:   #3b82f6;
  --accent:         #3b82f6;
  --accent-hover:   #2563eb;
  --shadow:         rgba(0, 0, 0, 0.08);
}

/* System dark mode (automatic, respects OS) */
@media (prefers-color-scheme: dark) {
  :root {
    --bg-base:     #0f172a;
    --bg-surface:  #1e293b;
    --bg-elevated: #334155;
    --text-primary:   #f1f5f9;
    --text-secondary: #94a3b8;
    --text-muted:     #64748b;
    --border:         #334155;
    --border-focus:   #60a5fa;
    --accent:         #60a5fa;
    --accent-hover:   #93c5fd;
    --shadow:         rgba(0, 0, 0, 0.3);
  }
}

/* User toggle dark mode (overrides OS preference) */
[data-theme="dark"] {
  --bg-base:     #0f172a;
  --bg-surface:  #1e293b;
  --bg-elevated: #334155;
  --text-primary:   #f1f5f9;
  --text-secondary: #94a3b8;
  --text-muted:     #64748b;
  --border:         #334155;
  --border-focus:   #60a5fa;
  --accent:         #60a5fa;
  --accent-hover:   #93c5fd;
  --shadow:         rgba(0, 0, 0, 0.3);
}

[data-theme="light"] {
  /* Force light mode even on dark OS */
  color-scheme: light;
  --bg-base:    #f8fafc;
  /* ... repeat light values */
}

Dark Mode Toggle in React

'use client';
import { useState, useEffect } from 'react';

type Theme = 'light' | 'dark' | 'system';

export function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as Theme) ?? 'system';
  });

  useEffect(() => {
    const root = document.documentElement;
    if (theme === 'system') {
      root.removeAttribute('data-theme');
    } else {
      root.setAttribute('data-theme', theme);
    }
    localStorage.setItem('theme', theme);
  }, [theme]);

  return { theme, setTheme };
}

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      aria-label="Toggle dark mode"
    >
      {theme === 'dark' ? '☀ Light' : '⏾ Dark'}
    </button>
  );
}

CSS Architecture for Large-Scale Applications

As applications grow, CSS architecture choices compound. Here are proven patterns for keeping styles maintainable across large codebases with multiple developers.

Design Token System

/* tokens/
 *   primitives.css   — raw color/spacing/size values
 *   semantic.css     — context-aware references to primitives
 *   component.css    — component-level tokens
 */

/* primitives.css */
:root {
  --primitive-blue-400: #60a5fa;
  --primitive-blue-500: #3b82f6;
  --primitive-red-500:  #ef4444;
  --primitive-space-4:  1rem;
  --primitive-space-6:  1.5rem;
}

/* semantic.css */
:root {
  --color-action:          var(--primitive-blue-500);
  --color-action-hover:    var(--primitive-blue-400);
  --color-danger:          var(--primitive-red-500);
  --spacing-content-gap:   var(--primitive-space-4);
  --spacing-section-gap:   var(--primitive-space-6);
}

/* component tokens — locally scoped to a component */
.button {
  --button-bg:       var(--color-action);
  --button-bg-hover: var(--color-action-hover);
  --button-padding:  0.5rem 1rem;

  background: var(--button-bg);
  padding: var(--button-padding);
}

.button:hover {
  background: var(--button-bg-hover);
}

/* Override component token for a specific context */
.hero .button {
  --button-bg: #fff;          /* white button in hero */
  --button-bg-hover: #f0f9ff;
  color: var(--color-action);
}

File Organization

/* Recommended file structure for a large Next.js app */
src/
├── styles/
│   ├── globals.css          /* @layer reset; @layer base; tokens */
│   ├── tokens/
│   │   ├── primitives.css
│   │   ├── semantic.css
│   │   └── dark.css
│   └── utilities.css        /* @layer utilities; */
├── components/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.module.css
│   │   └── Button.test.tsx
│   ├── Card/
│   │   ├── Card.tsx
│   │   └── Card.module.css
│   └── Modal/
│       ├── Modal.tsx
│       └── Modal.module.css
└── app/
    ├── layout.tsx
    └── page.tsx

CSS Linting with Stylelint

// .stylelintrc.json
{
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-css-modules"
  ],
  "plugins": [
    "stylelint-order"
  ],
  "rules": {
    "order/properties-order": [
      "content",
      "display",
      "position",
      "top", "right", "bottom", "left",
      "z-index",
      "flex", "flex-direction", "align-items", "justify-content",
      "grid", "grid-template-columns", "grid-template-rows",
      "width", "height", "min-width", "max-width",
      "margin", "padding",
      "background", "color",
      "font-size", "font-weight", "line-height",
      "border", "border-radius",
      "box-shadow",
      "transition", "animation"
    ],
    "custom-property-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
    "selector-class-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*(__[a-z0-9-]+)?(--[a-z0-9-]+)?$",
    "no-descending-specificity": true
  }
}

Comparison Table: BEM vs CSS Modules vs Tailwind vs CSS-in-JS

CriterionBEMCSS ModulesTailwindCSS-in-JS
ScopingNaming conventionBuild-time hashInline utility classesRuntime hash / static extract
Runtime costNoneNoneNone (PurgeCSS)Medium (runtime) / None (vanilla-extract)
Dynamic stylesVia JS className toggleVia className toggleVia className toggleNative via props
TypeScript supportNoneType-safe via .d.tsPartial (cva)Full (prop types)
Learning curveLowLowMedium (class names)Medium-High
Best forMulti-dev plain CSS / SassReact/Next.js component stylesRapid prototyping, design systemsHighly dynamic, prop-driven styles
DebuggingReadable class namesHashed names (source maps)Long class listsAuto-generated class names
Code splittingManual (imports)Automatic per moduleSingle CSS bundleComponent-level (runtime)
Tree shakingNoNo (import all)Yes (PurgeCSS)Yes (only used components)
DX (colocated)No (separate CSS file)Separate .module.cssSame file (JSX classes)Same file (styled/css prop)

Frequently Asked Questions

What is BEM and when should I use it?

BEM (Block Element Modifier) structures class names as block__element--modifier. Use it when your project uses global CSS without a build-time scoping solution. BEM prevents naming collisions through convention rather than tooling. If you are using React with CSS Modules or CSS-in-JS, BEM is largely redundant since scoping is automatic.

What is the difference between CSS Modules and CSS-in-JS?

CSS Modules scope class names at build time with zero runtime cost. CSS-in-JS (styled-components, emotion) writes CSS in JavaScript, enabling prop-driven dynamic styles, but with runtime injection overhead. vanilla-extract is a zero-runtime alternative that gives CSS-in-JS type-safety without the performance penalty.

When should I use CSS Grid vs Flexbox?

Use Grid for two-dimensional layouts (page skeleton, card grids, dashboards). Use Flexbox for one-dimensional layouts (navbars, button groups, centering, component-level alignment). They are complementary — a Grid page can contain Flexbox components.

How do CSS custom properties enable theming?

Define semantic tokens (--text-primary, --bg-surface) on :root and override them in @media (prefers-color-scheme: dark) or [data-theme="dark"]. Components reference only the semantic tokens, so the entire theme switches by changing token values — no component CSS needs to change.

What are the pros and cons of Tailwind CSS?

Pros: no naming decisions, no dead CSS, rapid prototyping, consistent design system, excellent developer experience in component frameworks. Cons: verbose HTML, harder to read at a glance, reusing style combinations requires component extraction, steep initial learning curve for the class names.

How do CSS animations affect performance?

Only transform and opacity are truly free — they run on the GPU compositor thread without layout or paint. Animating width, height, top, left triggers layout recalculation on every frame, causing drops below 60 fps on lower-end devices. Use will-change: transform to promote animated elements to their own GPU layer.

What are cascade layers and why do they matter?

@layer defines explicit CSS priority tiers. Styles in a later-declared layer always win over earlier layers, regardless of specificity within each layer. This eliminates specificity wars when combining reset styles, base styles, third-party component libraries, your own components, and utility overrides.

How do I implement dark mode in CSS?

Use semantic CSS custom properties on :root for light mode values. Override them inside @media (prefers-color-scheme: dark) for automatic OS-based dark mode. Add a second override on [data-theme="dark"] for user-toggled dark mode. Set color-scheme: light dark on :root so browser chrome (scrollbars, form inputs) follows the theme.

𝕏 Twitterin LinkedIn
Var detta hjälpsamt?

Håll dig uppdaterad

Få veckovisa dev-tips och nya verktyg.

Ingen spam. Avsluta när som helst.

Try These Related Tools

{ }CSS Minifier / Beautifier🌈CSS Gradient Generator

Related Articles

Tailwind CSS Guide: Utility Classes, Dark Mode, v4, and React/Next.js Integration

Master Tailwind CSS. Covers utility-first approach, responsive design, flexbox/grid utilities, dark mode, custom configuration, Tailwind v4 changes, React/Next.js integration with shadcn/ui, and Tailwind vs Bootstrap vs CSS Modules comparison.

React Hooks komplett guide: useState, useEffect och Custom Hooks

Beharska React Hooks med praktiska exempel. useState, useEffect, useContext, useReducer, useMemo, useCallback, custom hooks och React 18+ concurrent hooks.

Svelte Guide: Reactivity, Stores, SvelteKit, and Svelte 5 Runes

Master Svelte framework. Covers compiler approach, reactive statements and stores, component props and events, SvelteKit routing, transitions, state management, and Svelte vs React vs Vue vs SolidJS comparison.