CSS Modules & CSS Nesting

Published on

Another that could be considered in the “React Offramps” series I have been working on.

CSS Modules are yet another solution for styling that seems primarily suited to solve problems pretty specific to React. But it is technically a spec not a framework specific thing. Vue, Astro, and Solid all support it too. It is certainly one of the least worrisome styling solutions out there for React. Letting you write mostly plain ol CSS. In my experience this makes components much more portable to more modern frameworks.

My main gripe with it is that using the cascade of CSS in side of CSS modules. is … clunky. Disclaimer, I am not aiming to shit talk this tool which definitely was solving a real problem, but it’s almost as if the intended use of CSS modules is to mostly to pretend CSS has no cascade at all - at least in terms of classes. If I am wrong on that then I think the docs, which are pretty sparse could stand to be improved in this regard.

https://mastodon.social/@aeischeid/111047988985939754

Here is the issue: native CSS recently got some great new options that, from my perspective, makes a lot of the problems 2015 era CSS modules was aiming to solve, obsolete. That is to say, these are non-problems with CSS in 2024 and beyond. With a way to organize and maintain CSS in a way that is pretty self contained, dare I say modular, the value proposition of CSS modules just goes down significantly - if not completely. There are multiple new CSS features that potentially help solve modularity, but two stand out. First is CSS nesting. Second is @scope . Both are well supported in all modern browsers. Scope is sort of equally cool, but for the rest of this post I will be focusing on nesting as it relates to CSS modules.

Just with CSS nesting, I would almost say just stop using CSS modules, except there is at least one important feature it still offers - at least in a Next.js app. Because of the app bundles being split up by routes, true in Next.js & frameworks like it (Nuxt, SvelteKit, etc), the resources for many components may not ever need to be loaded load for a given user if they don’t visit those routes. Therefore putting all components’ css either into a global.css file or @importing them into a global css file that loads for all users regardless of if they will ever utilize that css isn’t ideal. Though unused CSS is probably less of a concern than JS, and well nested native CSS has a smaller footprint than old approaches to CSS nesting via older pre-processors, still it’s bytes that would be better loaded when needed.

Very very recently Next.js got laxer rules on CSS modules so that technically you can do CSS nesting in its version of CSS modules,

https://mastodon.online/@mshiltonj/112112719068685098

A great step, but still, because CSS modules mutates any class selectors in a module.css file anything other than nesting primitive element selectors is still quite clunky, to the point it just isn’t worth doing nesting.

The only way I have been able to figure out to do it is to us the so called :global to escape the hashing that css modules wants to perform on every class name in the module.css file. That hashing which is intended to keep css rules from leaking out of the component to the rest of the page is no longer needed when the rules are already nested in a class selector that is hashed.

.myModal {
	:global(.MuiDialog-paper) {
		padding: 22px;
		border-radius: 16px;
	}

	:global(.title) {
		margin: 24px;

		h2 { /* no need to :global escape primitive nested html selectors */
			font-size: 34px;
			padding-top: 24px;
			margin: 0;
			font-family: Inter;
			font-weight: 700;
			line-height: 42px;
		}

		:global(h2.primary) {
			color: var(--palette-primary-main);
		}

		:global(h2.info) {
			color: var(--palette-info-main);
		}
	}

	:global(.content) {
		margin: 24px;
	}

	:global(.actions) {
		margin: 24px;
		bottom: 0;
	}

	:global(.closeIcon) {
		position: absolute;
		right: 16px;
		top: 16px;
		color: var(--palette-secondary-800);
	}
}

but now, besides mucking up the src code file, the :global keyword is a misnomer! Because of the nesting, it doesn’t make these classes apply in the global CSS scope it just means something like :skipProcessing or :escape, but maybe css modules should just do that automatically for class selectors nested inside of other class selectors?

Alternatively I could write this in plain css

.myModal {
	.MuiDialog-paper {
		padding: 22px;
		border-radius: 16px;
	}

	.title {
		margin: 24px;

		h2 {
			font-size: 34px;
			padding-top: 24px;
			margin: 0;
			font-family: Inter;
			font-weight: 700;
			line-height: 42px;
		}

		h2.primary {
			color: var(--palette-primary-main);
		}

		h2.info {
			color: var(--palette-info-main);
		}
	}

	.content {
		margin: 24px;
	}

	.actions {
		margin: 24px;
		bottom: 0;
	}

	.closeIcon {
		position: absolute;
		right: 16px;
		top: 16px;
		color: var(--palette-secondary-800);
	}
}

This has almost all the same runtime advantages of the modules.css approach, and, of course, if I still want css files that correspond 1-to-1 with component files I can put that in a MyModal.css, import it in my global.css via @import url("MyModal.css");. The only other change I need to make is in my component file removing the module.css import and changing from className={styles.myModal} to className="myModal"

However, there is a downside here I alluded to earlier. If I want the runtime optimization of only importing css into layouts or pages that might actually use that css, well, if Using Next.js app directory approach it looks like you can do that, removing maybe the last reason you would bother with CSS modules, but options seem to be much more limited if you are still using NextJs’ old pages directory approach.