Claude Code for CSS Modules: Scoped Styles in React — Claude Skills 360 Blog
Blog / Frontend / Claude Code for CSS Modules: Scoped Styles in React
Frontend

Claude Code for CSS Modules: Scoped Styles in React

Published: April 5, 2027
Read time: 6 min read
By: Claude Skills 360

CSS Modules scope styles to the component file — import styles from "./Button.module.css" makes styles.button a unique class name like Button_button__abc12, preventing collisions. composes: base from "./base.module.css" inherits styles from another module. :global(.someClass) creates unscoped selectors. TypeScript type generation with typescript-plugin-css-modules or @types/css-modules provides autocomplete. CSS custom properties (--color-primary) work normally inside modules and can be combined with design tokens. clsx or cn combines module class names conditionally: clsx(styles.button, isActive && styles.active). Keyframe animations defined in modules are also scoped. Vite and Next.js support CSS Modules out of the box for .module.css files. Claude Code generates CSS Module files, TypeScript-typed imports, responsive component styles, and animation patterns for zero-runtime scoped styling.

CLAUDE.md for CSS Modules

## CSS Modules Stack
- Vite/Next.js: .module.css files auto-processed — no config needed
- TS types: add "typescript-plugin-css-modules" to tsconfig plugins for autocomplete
- Import: import styles from "./Component.module.css" — styles.className
- Conditional: className={clsx(styles.base, isActive && styles.active, className)}
- Compose: composes: base from "./shared.module.css" — style inheritance
- Global: :global(.slick-slide) { } — target third-party class names
- Custom props: var(--spacing-md) — CSS variables work normally
- Keyframes: @keyframes fadeIn defined in module — auto-scoped name

Component Example

/* components/ui/Button.module.css */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-2, 0.5rem);
  padding: var(--spacing-2, 0.5rem) var(--spacing-4, 1rem);
  border-radius: var(--radius-md, 0.375rem);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.25;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color 150ms ease, box-shadow 150ms ease, transform 100ms ease;
  white-space: nowrap;
  user-select: none;
}

.button:focus-visible {
  outline: 2px solid var(--color-ring, #3b82f6);
  outline-offset: 2px;
}

.button:active {
  transform: scale(0.98);
}

.button:disabled {
  opacity: 0.5;
  pointer-events: none;
}

/* Variants */
.primary {
  background-color: var(--color-primary, #3b82f6);
  color: var(--color-primary-foreground, #ffffff);
}

.primary:hover {
  background-color: color-mix(in srgb, var(--color-primary, #3b82f6) 85%, black);
}

.secondary {
  background-color: var(--color-secondary, #f1f5f9);
  color: var(--color-secondary-foreground, #0f172a);
  border-color: var(--color-border, #e2e8f0);
}

.secondary:hover {
  background-color: var(--color-secondary-hover, #e2e8f0);
}

.ghost {
  background-color: transparent;
  color: var(--color-foreground, #0f172a);
}

.ghost:hover {
  background-color: var(--color-accent, #f1f5f9);
}

.danger {
  background-color: var(--color-destructive, #ef4444);
  color: #ffffff;
}

/* Sizes */
.sm {
  padding: 0.375rem 0.75rem;
  font-size: 0.75rem;
  height: 2rem;
}

.md {
  height: 2.5rem;
}

.lg {
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  height: 3rem;
}

/* States */
.loading {
  pointer-events: none;
}

.fullWidth {
  width: 100%;
}

/* Loading spinner */
.spinner {
  animation: spin 0.75s linear infinite;
  width: 1em;
  height: 1em;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
// components/ui/Button.tsx — typed CSS module usage
import { clsx } from "clsx"
import styles from "./Button.module.css"
import type { ButtonHTMLAttributes } from "react"

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary" | "ghost" | "danger"
  size?: "sm" | "md" | "lg"
  isLoading?: boolean
  fullWidth?: boolean
}

export function Button({
  variant = "primary",
  size = "md",
  isLoading = false,
  fullWidth = false,
  disabled,
  className,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={clsx(
        styles.button,
        styles[variant],
        styles[size],
        isLoading && styles.loading,
        fullWidth && styles.fullWidth,
        className  // Allow external className override
      )}
      disabled={disabled || isLoading}
      aria-busy={isLoading || undefined}
      {...props}
    >
      {isLoading && (
        <svg
          className={styles.spinner}
          viewBox="0 0 24 24"
          fill="none"
          aria-hidden="true"
        >
          <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" opacity={0.25} />
          <path d="M4 12a8 8 0 018-8" stroke="currentColor" strokeWidth="4" strokeLinecap="round" />
        </svg>
      )}
      {children}
    </button>
  )
}

Card Component with Composes

/* components/ui/Card.module.css */

/* Base styles shared via composes */
.base {
  border-radius: var(--radius-lg, 0.5rem);
  border: 1px solid var(--color-border, #e2e8f0);
  background-color: var(--color-card, #ffffff);
  overflow: hidden;
}

.card {
  composes: base;  /* Inherit base styles */
  padding: 1.5rem;
  transition: box-shadow 150ms ease, transform 150ms ease;
}

.interactive {
  composes: card;
  cursor: pointer;
}

.interactive:hover {
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  transform: translateY(-1px);
}

.header {
  padding: 1.25rem 1.5rem;
  border-bottom: 1px solid var(--color-border, #e2e8f0);
}

.body {
  padding: 1.5rem;
}

.footer {
  padding: 1rem 1.5rem;
  border-top: 1px solid var(--color-border, #e2e8f0);
  background-color: var(--color-muted, #f8fafc);
}

/* Media queries within modules */
@media (max-width: 640px) {
  .card {
    border-radius: 0;
    border-left: none;
    border-right: none;
  }
}

TypeScript Configuration

// tsconfig.json — CSS Modules autocomplete plugin
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-plugin-css-modules",
        "options": {
          "classnameTransform": "camelCaseOnly"
        }
      }
    ]
  }
}
// vite.config.ts — CSS Modules in Vite (works out of the box)
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"

export default defineConfig({
  plugins: [react()],
  css: {
    modules: {
      // Enable camelCase transformation: my-class → myClass
      localsConvention: "camelCaseOnly",
      // Include file hash in generated class names
      generateScopedName: "[name]__[local]__[hash:base64:5]",
    },
  },
})

Animation Patterns

/* components/animations/FadeIn.module.css — scoped animations */

@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

.fadeUp {
  animation: fadeUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.slideIn {
  animation: slideIn 0.3s ease-out forwards;
}

.skeleton {
  animation: pulse 1.5s ease-in-out infinite;
  background: linear-gradient(90deg,
    var(--color-muted, #f1f5f9) 25%,
    var(--color-muted-hover, #e2e8f0) 50%,
    var(--color-muted, #f1f5f9) 75%
  );
  background-size: 200% 100%;
}

/* Stagger children: nth-child delays */
.staggerChildren > :nth-child(1) { animation-delay: 0ms; }
.staggerChildren > :nth-child(2) { animation-delay: 60ms; }
.staggerChildren > :nth-child(3) { animation-delay: 120ms; }
.staggerChildren > :nth-child(4) { animation-delay: 180ms; }
.staggerChildren > :nth-child(5) { animation-delay: 240ms; }

/* Respect user's motion preference */
@media (prefers-reduced-motion: reduce) {
  .fadeUp, .slideIn { animation: none; opacity: 1; transform: none; }
}

For the Tailwind CSS alternative when utility-first styling with design system tokens, a purge-enabled production build, and no per-component CSS files are preferred — Tailwind co-locates styles in JSX as className strings and eliminates the need for .module.css files entirely, see the Tailwind guide. For the vanilla-extract alternative when CSS-in-TypeScript with full type safety, design token themes (createGlobalTheme), and sprinkles for atomic CSS generation are needed — vanilla-extract statically extracts to plain CSS at build time like CSS Modules but with TypeScript-authored styles, see the type-safe CSS guide. The Claude Skills 360 bundle includes CSS Modules skill sets covering variants, animations, and TypeScript integration. Start with the free tier to try scoped styling generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free