CSS Modules Alternatives: What a 0-Module Codebase Ships Instead
Utilities, CSS-in-JS, BEM or native @scope: what each gives up. This repo has no module files and ships one 83 KB stylesheet (14.8 KB gzipped) for every page.
The realistic alternatives to CSS Modules are utility classes (Tailwind), CSS-in-JS, a naming convention such as BEM, and native CSS features like @scope and @layer. Which one fits depends on what you want to keep: CSS Modules gives you hashed, per-file class names. This storefront dropped hand-written component CSS altogether, and its whole production stylesheet is 83,365 bytes (14,803 gzipped), shared by every page.
This post is the "what did we pick instead, and what did it cost" version. The numbers below come from a npm run build of this repository, not from a benchmark suite.
The short answer
| If you want... | Reach for | What you give up |
|---|---|---|
| Collision-proof class names, still writing plain CSS | CSS Modules (stay put) | Nothing, but you keep authoring a stylesheet per component |
| No stylesheet authoring at all | Tailwind utility classes | Long className strings; a design-token discipline |
| Styles that depend on runtime values | CSS custom properties or a small inline style | Not "pure" static CSS |
| Scoping without a build tool | Native @scope and @layer | Newer syntax; you still write the CSS |
| Styles computed in JavaScript | CSS-in-JS (Emotion, styled-components) | A runtime or build plugin, and Server Component friction |
| Zero tooling, small team | BEM naming convention | Discipline instead of enforcement |
If your complaint with CSS Modules is the ceremony (a .module.css file next to every component, an import, a styles. prefix on every class), utilities remove the ceremony. If your complaint is that you want scoping and no build step, the native route is the one to watch. If you like Modules and just want nicer variables, you probably do not need an alternative; plain CSS custom properties work inside a module.
What CSS Modules actually does
A CSS Modules file is ordinary CSS where every class name is rewritten at build time into something unique. .button in one file and .button in another stop colliding because the bundler emits .button_a3f01 and .button_9c2d7. That is the whole feature: scoping. It is not a styling system, has no design tokens, and does not help you write less CSS.
That framing matters because most "alternative" lists compare tools that solve different problems. A preprocessor (Sass) extends the syntax. A utility framework (Tailwind) removes the need to name things. CSS-in-JS moves rules into components. Only the last two are real replacements for the scoping job; the first is a companion. If you are weighing a preprocessor specifically, the repo's earlier write-ups cover it: CSS Modules vs. Sass and Sass vs. Tailwind.
What this repo uses instead
Count first:
git ls-files '*.module.css' '*.scss' '*.sass' | wc -l # 0
find src -name '*.css' # src/app/globals.css
Zero module files, zero preprocessor files, and exactly one stylesheet in src/: src/app/globals.css, which holds the @theme design tokens, the responsive .container rule and two small utilities. There are 139 .tsx files under src/, and none of them imports a stylesheet of its own. Every component styles itself with Tailwind classes in its own className.
The page-width wrapper is the clearest example of where a Module would have gone. A .container rule is global on purpose, and it is reached through a single atom so the class name is only ever written once:
// src/components/atoms/Container.tsx
export default function Container({ className = "", children }) {
return <div className={`container ${className}`.trim()}>{children}</div>;
}
That is the compromise worth copying: if you have one or two truly global rules, keep them in one stylesheet and wrap them in a component, rather than scattering a module per component to protect a name that only exists once.
The measured cost
After npm run build, the output contains one CSS chunk:
| Measure | Value |
|---|---|
CSS files emitted in .next/static/chunks/ | 1 |
| Raw size | 83,365 bytes |
Gzipped (gzip -9) | 14,803 bytes |
Stylesheets linked from /, /pricing, /templates | 1 each, the same file |
Two things follow from that. First, utility CSS is bounded by the vocabulary you use rather than by the number of components, so adding a page mostly reuses classes that are already in the file. A per-component CSS Modules setup grows with the component count, because each module is its own rule set even when two components share the same padding and colour. Second, one shared stylesheet means a visitor who lands on / has already downloaded the CSS for /pricing. That is a trade, not a free win: you ship rules for pages the visitor has not opened yet, and you give up per-route CSS splitting.
I am deliberately not claiming the utility approach is smaller than a well-built Modules setup. This repo has no Modules build to compare against, so there is no honest side-by-side number. What the measurement supports is narrower: with utilities, the stylesheet is one cacheable file whose size you can read off the build.
Where inline style still earns its place
Utilities cannot express a value computed at runtime, because Tailwind generates classes from source text at build time. A class like translate-x-[${index * 100}%] never gets generated. This repo has 55 style={{ sites, and the real dynamic ones are small:
// src/components/molecules/TemplateGallery.tsx
<div
className="flex transition-transform duration-500 ease-out motion-reduce:transition-none"
ref={trackRef}
style={{ transform: `translateX(-${index * 100}%)` }}
>
The static parts of the slide track (flex, transition timing, the reduced-motion override) stay as utilities; only the one value that depends on the current slide index is inline. The preview modal does the same for its device frame, setting width, height and transform: scale(...) from measured stage size. If you are leaving CSS Modules and find yourself reaching for CSS-in-JS only for dynamic values like these, an inline style or a CSS custom property set from JavaScript is the lighter option.
Component-level responsiveness without a module
One thing Modules people do not expect to lose: component-scoped media queries. The answer here is container queries. A card marks itself as a query container and its children respond to the card's width rather than the viewport's:
// src/components/molecules/TechStackCard.tsx
<div className="group @container rounded-3xl border ...">
Tailwind v4 ships @container and the @md: style variants in core, so the scoped-by-component behaviour that Modules plus a media query used to approximate is a utility. For the deeper version of this, see Tailwind container queries.
Mistakes and troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
A dynamic class (w-[${n}px]) never applies | Tailwind scans source text; interpolated class names are not generated | Use an inline style, or map to a fixed set of full class names |
| A global rule collides with a component after migration | The old module name and a new global rule share a selector | Put the rule in a wrapper component, as Container does |
| Styles from a deleted module still appear | A global stylesheet kept a copy | Grep the class name across src/ before deleting it |
| The CSS file is larger than expected | Utilities for every page ship in one file | Check the build output; split by route only if the number justifies it |
| Dark mode breaks on a migrated component | The module used a media query, the utilities need the dark: variant | Add dark: variants alongside the light classes |
Frequently asked questions
Is CSS Modules deprecated? No. It is a stable, widely supported feature in Next.js and Vite. Moving away is a preference about authoring style, not a requirement.
Can I use Tailwind and CSS Modules together? Yes. They are independent: a module for the handful of rules that are awkward as utilities, Tailwind for everything else. This repo simply does not need the module half.
What is the lightest alternative if I do not want a framework?
Plain CSS with a naming convention, plus @layer to control specificity order. You keep zero dependencies and give up automatic scoping.
Does Tailwind work in Server Components? Yes. It is static CSS, so it has no runtime and no hydration cost, unlike runtime CSS-in-JS libraries.
Templates in this post
ASoc Till is a POS-system marketing site with a live register preview and app-store download calls to action. ASoc Timbre is an AI voice-generator landing page with voice samples, 170+ languages and pricing tiers. ASoc Uptime is a web-hosting site with domain search, performance tooling and tiered pricing.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
