css-modules
Implements scoped CSS using CSS Modules with automatic class name generation, composition, and TypeScript support. Use when needing component-scoped styles, avoiding naming collisions, or migrating from global CSS.
When & Why to Use This Skill
This Claude skill enables the seamless implementation of locally scoped CSS through CSS Modules, automatically generating unique class names to eliminate global namespace collisions. It streamlines the creation of maintainable, component-based styles with robust support for composition and TypeScript, ensuring type-safe styling workflows in modern web frameworks like React, Next.js, and Vite.
Use Cases
- Component-Scoped Styling: Develop isolated styles for UI components to ensure that CSS rules remain encapsulated and do not cause unintended side effects across the application.
- Legacy CSS Migration: Systematically migrate large-scale projects from fragile global CSS to a modular architecture, reducing technical debt and styling bugs.
- TypeScript Type Safety: Integrate with typed-css-modules to provide autocompletion and compile-time error checking for CSS class names within TypeScript files.
- Style Composition and Reuse: Utilize the 'composes' feature to build complex styles from reusable base modules, maintaining a DRY (Don't Repeat Yourself) and scalable CSS codebase.
- Dynamic Theming: Implement themeable UI components by combining CSS Modules with CSS variables and global selectors for flexible design systems.
| name | css-modules |
|---|---|
| description | Implements scoped CSS using CSS Modules with automatic class name generation, composition, and TypeScript support. Use when needing component-scoped styles, avoiding naming collisions, or migrating from global CSS. |
CSS Modules
Locally scoped CSS by generating unique class names at build time.
Quick Start
CSS Modules work out of the box in Vite, Next.js, and Create React App.
Create module file:
/* Button.module.css */
.button {
padding: 12px 24px;
border-radius: 8px;
border: none;
cursor: pointer;
}
.primary {
background: #3b82f6;
color: white;
}
.secondary {
background: #e5e7eb;
color: #1f2937;
}
Import and use:
// Button.tsx
import styles from './Button.module.css';
interface ButtonProps {
variant?: 'primary' | 'secondary';
children: React.ReactNode;
}
export function Button({ variant = 'primary', children }: ButtonProps) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
File Naming Convention
ComponentName.module.css # Component styles
page.module.css # Page styles
layout.module.css # Layout styles
Files without .module.css are treated as global CSS.
Accessing Classes
import styles from './Component.module.css';
// Single class
<div className={styles.container} />
// Multiple classes
<div className={`${styles.card} ${styles.elevated}`} />
// Conditional classes
<div className={`${styles.button} ${isActive ? styles.active : ''}`} />
// Dynamic class from variable
const variant = 'primary';
<div className={styles[variant]} />
// With clsx/classnames library
import clsx from 'clsx';
<div className={clsx(styles.button, {
[styles.active]: isActive,
[styles.disabled]: isDisabled,
})} />
Class Name Conventions
CSS class names become JavaScript property names:
/* Valid patterns */
.button { } /* styles.button */
.primaryButton { } /* styles.primaryButton */
.primary-button { } /* styles['primary-button'] or styles.primaryButton (with camelCase) */
.Button { } /* styles.Button */
/* Avoid - invalid JS identifiers */
.123start { } /* Won't work */
.my class { } /* Won't work */
Composition
Same File Composition
/* base.module.css */
.base {
padding: 16px;
border-radius: 8px;
font-family: system-ui;
}
.card {
composes: base;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.panel {
composes: base;
background: #f3f4f6;
border: 1px solid #e5e7eb;
}
Cross-File Composition
/* typography.module.css */
.heading {
font-weight: 700;
line-height: 1.2;
}
.body {
font-weight: 400;
line-height: 1.6;
}
/* Card.module.css */
.title {
composes: heading from './typography.module.css';
font-size: 1.5rem;
margin-bottom: 8px;
}
.description {
composes: body from './typography.module.css';
color: #6b7280;
}
Composing Multiple Classes
.special {
composes: base highlight from './shared.module.css';
composes: bordered from './borders.module.css';
}
Global Class Composition
.button {
/* Compose from global CSS (like a reset or utility) */
composes: reset-button from global;
padding: 12px 24px;
}
Global Styles
:global Selector
/* Within a module file */
:global(.body-locked) {
overflow: hidden;
}
/* Mixed local and global */
.modal :global(.overlay) {
background: rgba(0,0,0,0.5);
}
/* Global block */
:global {
.utility-class {
display: flex;
}
.another-utility {
gap: 16px;
}
}
:local Selector (explicit)
:local(.className) {
/* Explicitly local (default behavior) */
}
Nesting with PostCSS
With PostCSS nesting or native CSS nesting:
.card {
padding: 16px;
.header {
display: flex;
justify-content: space-between;
}
.body {
margin-top: 12px;
}
&:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
&.highlighted {
border: 2px solid #3b82f6;
}
}
TypeScript Support
Declaration File
// css-modules.d.ts (in src/ or types/)
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
typed-css-modules
Generate type definitions from CSS files:
npm install -D typed-css-modules
npx tcm src
Generates .css.d.ts files:
// Button.module.css.d.ts
declare const styles: {
readonly button: string;
readonly primary: string;
readonly secondary: string;
};
export default styles;
Watch mode:
npx tcm src --watch
Options:
# CamelCase class names
npx tcm src --camelCase
# Named exports (for tree shaking)
npx tcm src --namedExports
# TypeScript 5 arbitrary extensions
npx tcm src --allowArbitraryExtensions
typescript-plugin-css-modules
IDE support without generating files:
// tsconfig.json
{
"compilerOptions": {
"plugins": [
{ "name": "typescript-plugin-css-modules" }
]
}
}
Framework Configuration
Vite
Works out of the box. Custom configuration:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
css: {
modules: {
// Generate scoped class names
generateScopedName: '[name]__[local]___[hash:base64:5]',
// Export class names as camelCase
localsConvention: 'camelCase',
},
},
});
Next.js
Works out of the box. No configuration needed.
Webpack (manual)
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
},
},
],
},
],
},
};
Patterns
Component with Variants
/* Alert.module.css */
.alert {
padding: 12px 16px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
}
.info {
composes: alert;
background: #dbeafe;
color: #1e40af;
}
.success {
composes: alert;
background: #dcfce7;
color: #166534;
}
.warning {
composes: alert;
background: #fef3c7;
color: #92400e;
}
.error {
composes: alert;
background: #fee2e2;
color: #991b1b;
}
import styles from './Alert.module.css';
type AlertVariant = 'info' | 'success' | 'warning' | 'error';
interface AlertProps {
variant: AlertVariant;
children: React.ReactNode;
}
export function Alert({ variant, children }: AlertProps) {
return <div className={styles[variant]}>{children}</div>;
}
Responsive Styles
.container {
padding: 16px;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.container {
padding: 24px;
}
.grid {
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
}
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
Theming with CSS Variables
.card {
background: var(--card-bg, white);
color: var(--card-text, #1f2937);
border: 1px solid var(--card-border, #e5e7eb);
border-radius: var(--radius-md, 8px);
padding: var(--spacing-4, 16px);
}
/* Dark theme override via parent */
:global(.dark) .card {
--card-bg: #1f2937;
--card-text: #f9fafb;
--card-border: #374151;
}
Best Practices
- One module per component - Keep styles co-located
- Use composition - Share styles via
composes - Avoid deep nesting - Max 2-3 levels
- Meaningful class names -
.submitButtonnot.btn1 - TypeScript types - Use typed-css-modules or plugin
Common Mistakes
| Mistake | Fix |
|---|---|
Missing .module.css extension |
Rename to *.module.css |
| Accessing undefined class | Check spelling, add TypeScript |
| composes after other rules | Move composes to top of rule |
| Circular composition | Restructure dependencies |
| Styling by element | Use class selectors instead |
Reference Files
- references/composition.md - Advanced composition
- references/typescript.md - TypeScript integration