Website markup using CSS Modules
CSS Modules solve one specific problem — global CSS namespace. In any project with more than three developers or more than a hundred components, class names start conflicting and overriding each other, and !important spreads through the codebase. CSS Modules provide local scope at build time — without runtime overhead, without shadow DOM.
How it works
Vite, Webpack, Parcel, and other bundlers transform CSS Modules at build time. Class name in .module.css file is hashed:
.button → .button_a3f7k2x
.title → .title_9cxm1pq
Final HTML contains hashed names, conflict is impossible by definition.
/* Button.module.css */
.root {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: var(--color-accent);
color: #fff;
border-radius: 0.5rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: background-color 150ms ease;
}
.root:hover {
background-color: var(--color-accent-hover);
}
.root:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Variants */
.ghost {
background-color: transparent;
color: var(--color-accent);
border: 1px solid var(--color-accent);
}
/* Sizes */
.sm {
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
}
.lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
// Button.tsx
import { FC, ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';
import clsx from 'clsx';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
const Button: FC<ButtonProps> = ({
variant = 'default',
size = 'md',
className,
children,
...props
}) => {
return (
<button
className={clsx(
styles.root,
variant === 'ghost' && styles.ghost,
size === 'sm' && styles.sm,
size === 'lg' && styles.lg,
className
)}
{...props}
>
{children}
</button>
);
};
Setup in Vite
CSS Modules work out of the box — any file *.module.css is processed automatically:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
css: {
modules: {
// camelCase for JS access: styles.myClass instead of styles['my-class']
localsConvention: 'camelCase',
// Format of generated class names
generateScopedName:
process.env.NODE_ENV === 'production'
? '[hash:base64:8]'
: '[name]__[local]__[hash:base64:4]',
},
},
});
Pattern: composes for reuse
CSS Modules support composes — style inheritance without JavaScript:
/* base.module.css */
.flexCenter {
display: flex;
align-items: center;
justify-content: center;
}
.card {
background: var(--color-bg-surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}
/* Hero.module.css */
.container {
composes: flexCenter from './base.module.css';
padding: 3rem 1rem;
min-height: 100vh;
}
.title {
composes: card from './base.module.css';
font-size: 2.5rem;
font-weight: bold;
}
Advantages
- Zero runtime cost — compiled away at build time
- Local scope — no BEM or naming conventions needed
- Dead code elimination — unused styles removed in production
- Dynamic theming — combine with CSS Custom Properties
Timeline
Integrating CSS Modules into existing project: 2–3 days. New project with full component library: included in component development timeline.







