Name a file *.module.css and the compiler rewrites every class selector in it to a name that cannot collide with anything else in the application, then gives you an object mapping your names to the generated ones.
/* app/card/card.module.css */
.card { padding: 16px; border: 1px solid #cfd8dc; border-radius: 8px; }
/* app/card/page.tsx */
import styles from "./card.module.css";
export default function Page() {
return <article className={styles.card}>Scoped by the compiler</article>;
}The generated selector carries the file name, a hash of the file's path and contents, and your original class name, so a DevTools inspection still tells you which file to open:
.card-module__71VQKa__card{border:1px solid #cfd8dc;border-radius:8px;padding:16px}
<article class="card-module__71VQKa__card">Three things follow from this being a compile-time transform. The import yields a plain object, so a Server Component can use it: no runtime library, nothing to hydrate. Dynamic class names must be looked up on that object — styles[variant] works, the string ` styles.${variant} ` does not. And because the hash covers the file contents, editing the CSS changes the class name, so the stylesheet can be served immutable.
Selectors that are not class names — element selectors, :root, attribute selectors — are not scoped. A bare h2 { } in a module is a global rule with a misleading file name; nest it under a scoped class. Conversely, :global(.leaflet-popup) leaves a selector alone, for markup a third-party library generates.