Remix scopes CSS to the routes that actually need it

The hardest problem in CSS at scale is predicting side effects. Because styles cascade, any rule you add can leak into pages you never intended to touch. The usual defenses—naming conventions, preprocessors, CSS modules, CSS-in-JS—all try to contain that unpredictability. Remix takes a more direct route: it ties a stylesheet’s lifetime to a route’s lifetime.

Imagine you want every h1 on an “About” page to be blue. In a traditional setup you’d namespace the rule with a parent class like about-page so it doesn’t affect headings elsewhere, then include that CSS everywhere, even on pages where it does nothing. That’s wasteful on the network and risky for maintenance: a future change to that file could have effects on any page that loads it.

Remix changes the contract. When you import a .css file in a route module, you receive a URL. You then export that URL through the route’s links export:

import type { LinksFunction } from 'remix'
import aboutStyles from '#app/styles/routes/about.css'

export const links: LinksFunction = () => {
	return [{ rel: 'stylesheet', href: aboutStyles }]
}

export default function AboutScreen() {
	return <stuff />
}

That tells Remix: when this route is active, inject the corresponding <link> tag. When the user navigates away, remove it. No JavaScript involved in the CSS itself—Remix just manages the tags for you. The result is that a CSS file is only present in the browser while its route is on screen. Navigate from /about to the homepage and the stylesheet vanishes with the route, restoring the original heading color.

This has a profound effect on developer confidence. To know what a stylesheet impacts, you only need to check which routes import it. In the common case, that’s a single page, which means you can edit styles without worrying you’ve broken something elsewhere. You also don’t need namespace conventions or automatic CSS scoping tools—Remix isn’t rewriting your styles, it’s simply loading and unloading them at the right moments.

What about shared components?

Styles for reusable components are a bit different. Since such a component can appear anywhere, its CSS needs to be available on every page where you use it. In practice, that usually means putting it in the root route so it’s loaded site-wide. That’s fine, but you should recognize the trade-off: once a stylesheet lives at the root, changes to it affect every page, and you’ll need to namespace accordingly relative to the other styles active there.

That tension is worth stating clearly: with Remix you can declare which pages a CSS file applies to, and you can statically determine those pages just by reading the route tree.

Tailwind is still a solid default

For this kind of project, utility CSS remains an attractive option. Tailwind sidesteps cascade collisions through its design system and brings a useful set of constraints for keeping a UI visually consistent. One-off styles, though, are exactly where Remix’s route-scoped CSS shines. If you don’t want to adopt a utility framework, knowing Remix handles loading and unloading for plain stylesheets gives you a straightforward path to predictable, maintainable styling.