CSS Modules & Scoped Styling
Scope CSS to components with CSS Modules: naming conventions, composition, TypeScript integration, and combining with global styles.
CSS Modules transform class names to unique identifiers at build time, giving you locally-scoped styles by default. Unlike CSS-in-JS, CSS Modules have zero runtime overhead and work with standard CSS syntax, making them ideal for performance-sensitive applications.
CSS Modules are the right default for most component libraries and apps: zero runtime overhead, standard CSS syntax, works with all CSS preprocessors, and TypeScript-friendly. Use them with camelCase class names for JS access.
Workflow
- Create and import CSS Modules filesThe `.module.css` extension tells the bundler to scope all class names.
- Compose and combine class namesComponents often need multiple conditional class names; a utility handles this cleanly.
- TypeScript integrationTypeScript can catch typos in CSS class name references at compile time.
- Handle animations and global stylesAnimations defined in CSS Modules are also scoped - you need to declare them correctly.
- Structure CSS Modules at scaleConsistent patterns prevent duplication and make maintenance easier.
Output Format
Co-located `.module.css` files per component, `clsx` for conditional classes, TypeScript typings, global styles in separate `global.css`, design tokens as CSS custom properties.
Examples
Component: `import styles from "./Card.module.css"; export function Card({ featured }: Props) { return <div className={clsx(styles.card, { [styles.featured]: featured })}><h2 className={styles.title}>{title}</h2></div> }`Composes: `.primaryButton { composes: button; background: var(--color-primary); color: white; } .secondaryButton { composes: button; background: transparent; border: 1px solid var(--color-primary); }`TypeScript: `// vite-env.d.ts: declare module "*.module.css" { const classes: { readonly [key: string]: string }; export default classes; }`Edge Cases & Gotchas
- SSR: Next.js and Remix both support CSS Modules with SSR out of the box - styles are extracted at build time; no runtime injection needed
- Dynamic class names: avoid string interpolation like `styles[`color-${variant}`]` - bundlers may not track these and will tree-shake the class. Use explicit conditionals instead
- Third-party component styling: you cannot CSS Module a class that a third-party library generates. Use `:global(.third-party-class)` inside your module to target it
When to Break These Rules
- For design systems with complex theming (multiple themes, dark mode, dynamic user themes), CSS-in-JS (styled-components, Emotion) with theme providers gives more flexibility despite the runtime cost.
Reference Files
CSS Modules spec: https://github.com/css-modules/css-modulesVite CSS Modules: https://vitejs.dev/guide/features#css-modulestyped-css-modules: https://github.com/Quramy/typed-css-modulesclsx: https://github.com/lukeed/clsx