CSS Modules
CSS Modules give you locally scoped class names without runtime cost - the file system becomes your namespace and the build inlines hashes.
React - CSS Modules
EXAMPLE
// Button.module.css
.button {
background: #2563eb;
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
border: none;
cursor: pointer;
}
.button:hover { background: #1d4ed8; }
.button:disabled { opacity: 0.5; cursor: not-allowed; }
.primary { background: #16a34a; }
.primary:hover { background: #15803d; }
// Button.tsx
import styles from './Button.module.css';
import clsx from 'clsx';
type Props = {
variant?: 'default' | 'primary';
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
};
export function Button({ variant = 'default', disabled, children, onClick }: Props) {
return (
<button
className={clsx(styles.button, variant === 'primary' && styles.primary)}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
// Usage
import { Button } from './Button';
<Button>Cancel</Button>
<Button variant='primary'>Save</Button>
// vite.config.ts - already supported in Vite
// next.config.js - already supported in Next
// Optional: customise hash format
export default {
css: {
modules: {
generateScopedName: '[name]_[local]__[hash:base64:5]',
},
},
};
Why it matters
CSS Modules sit between plain CSS and CSS-in-JS. Zero runtime, no class-name collisions, and the styles ship as a normal stylesheet - which means CDN caching and no FOUC. Reach for them when Tailwind feels too coupled to markup and Emotion feels too heavy.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import s from './Button.module.css';
return <button className={s.primary}>Save</button>;
Try it Yourself »
Discussion
Loading…