Fixing CSS in React and Next.js Apps
React itself has no opinion about CSS, so every React project picks its own mix: global stylesheets, CSS Modules, a utility framework such as Tailwind, a CSS-in-JS library, or several at once.
Most of the CSS bugs specific to React come from that mix: styles that look right in development and wrong in production, a button that changes colour depending on which page you visited first, Tailwind classes that silently do nothing, and pages that flash unstyled before JavaScript runs. This guide explains why each happens in React and Next.js builds and how to fix it, including when runtime CSS-in-JS is worth replacing.
Why React apps end up with CSS problems
A React component can bring its styles in four quite different ways, and each is processed at a different time:
| Approach | When styles are created | Typical problems |
|---|---|---|
| Global stylesheets imported into the app | Build time | Class name clashes, rules leaking between pages, order depending on imports |
CSS Modules (*.module.css) | Build time, with class names made unique | Order
between modules on different routes, overuse of :global |
| Utility framework such as Tailwind | Build time, generated from class names found in source | Classes built from strings are never generated; conflicting utilities on one element |
| Runtime CSS-in-JS such as styled-components or Emotion | In the browser, and on the server if configured | Flash of unstyled content, hydration mismatches, extra JavaScript and render work |
Problems cluster where two approaches meet: a global stylesheet overriding a module, a CSS-in-JS component fighting a Tailwind utility, or a third-party component library shipping its own theme on top of yours. The CSS Checker lists the stylesheets your built pages load, with their sizes, render-blocking and unused CSS, validation errors and specificity issues. In a React build that list shows which bundles each route pulls in, which is the starting point for everything below.
Development and production behave differently
The development server and the production build handle CSS differently, so a bug you see only in one is often not a bug in your CSS at all.
- In development, bundlers such as Vite and the Next.js dev server inject styles through JavaScript so they can hot-reload them. Pages briefly appear unstyled on a hard reload. That flash does not happen in a production build and is not worth fixing.
- In production, CSS is extracted into real stylesheet files, split into chunks and loaded with each route. The order in which chunks load can differ from the order you imported files in development, and that is where genuine ordering bugs appear.
Always confirm a CSS problem against a production build served locally, for example next build && next start
or vite build && vite preview, and look at the built output. In Next.js the extracted
files sit under .next/static/css/; in Vite they are in dist/assets/. Open the
page, filter the Network panel to CSS, and note which files each route loads.
CSS order between routes and chunks
When two rules with equal specificity target the same element, the later one wins. In a single hand-written stylesheet "later" is obvious. In a bundled React app, "later" depends on which chunks have loaded and in what order, and that can change depending on the page a visitor lands on and how they navigate.
A typical case: Button.module.css sets the button colour, and a page-level module for
the pricing page also styles buttons inside it with a single class. Visit the pricing page directly and
one file loads last; arrive from the home page and the other does. The button changes colour.
Fixes, from most to least robust:
- Do not style one component's internals from another file. Give the component a prop
or variant, such as
<Button variant="outline">, and keep every rule about the button in the button's module. - Use cascade layers for groups of styles. Declare
@layer reset, base, components, pages, utilities;in the global stylesheet imported first, and wrap each file's rules in its layer. Layer order then decides conflicts, whatever order the chunks arrive in. - Import shared styles in one place. Global CSS, resets and design tokens go into the root layout or entry file, once, so they are always first.
- Check your framework's CSS chunking options. Next.js has configuration that affects how CSS is split and merged across routes; read the current documentation for your version before changing it, because the trade-off is between strict ordering and fewer, larger files.
For the general rules about which declaration wins, see Fixing CSS specificity conflicts.
The fix, step by step
Step 1
Identify every styling method in the app
Search the codebase for imports ending in .css, .module.css
and .scss, for styled and css imports from CSS-in-JS libraries,
for a Tailwind entry file, and for component libraries that ship their own styles. List each method with
where it is used. Most React CSS bugs appear where two methods style the same element, so this list tells
you where to look first and what can eventually be consolidated.
Step 2
Check the production CSS, not the dev server
Run a production build and serve it locally with next start or vite preview.
Run the CSS Checker against the built pages, then filter the Network panel to CSS and note which files
each route loads, their sizes and whether they block rendering. Load key pages directly and by navigating
from other pages. Any style that differs between the two routes points to a chunk ordering problem you
need to fix.
Step 3
Keep global CSS in one entry point
Import
resets, fonts, design tokens and any truly global rules once, in the root layout of a Next.js App Router
project, in _app for the Pages Router, or in the main entry file of a Vite app. Declare
your cascade layer order at the very top of that file. Remove global imports scattered through individual
components and pages, because they make the final order depend on which route happened to load first.
Step 4
Scope component styles with CSS Modules
Move component styles into a .module.css file beside each component, so class
names become unique at build time and cannot clash across the app. Keep each selector to a single local
class, express variations as extra classes chosen by props, and avoid reaching into child components
with :global. Where a parent needs to change a child's appearance, add a prop or variant
to the child instead of overriding it from the parent's module.
Step 5
Make every Tailwind class visible to the scanner
Tailwind only generates classes it can find as complete strings in your source.
Replace patterns such as bg-${colour}-600 with a lookup object that lists full class names.
Make sure folders outside the default scan, such as shared packages in a monorepo, are included with
@source or the content setting for your version. Use a helper such as tailwind-merge where
props can pass conflicting utilities to one element.
Step 6
Server-render or replace runtime CSS-in-JS
If you keep styled-components or Emotion, enable the library's server rendering support, including a style registry in the Next.js App Router and the compiler option that keeps class names consistent between server and client, so pages arrive styled and hydrate without mismatches. For new work or heavy components, prefer CSS Modules, Tailwind or a zero-runtime library that extracts static CSS at build time instead of generating styles during rendering.
Step 7
Pass dynamic values through custom properties
Where a style depends on data, such as a progress bar width, a brand colour from an
API or a chart segment size, set a CSS custom property through the style prop, for example
an object with a --progress key set to the value plus a percent sign, and read it in a static
rule with width: var(--progress). The stylesheet stays static and cacheable, React updates
one property, and no new class or style tag has to be generated at runtime.
Tailwind classes that do nothing
Tailwind reads your source files as plain text and generates CSS for every class name it finds. It never runs your code, so it cannot see a class name assembled at runtime:
// Tailwind never sees "bg-red-600" or "bg-green-600" here
<span className={`bg-${status === 'error' ? 'red' : 'green'}-600`}>
// Full class names in the source: both are generated
const statusClass = {
error: 'bg-red-600',
ok: 'bg-green-600'
};
<span className={statusClass[status]}>
The same applies to class names stored in a CMS or database. Either keep a list of them in a file
the scanner reads, or safelist them. In recent versions of Tailwind, the CSS-first setup uses @import "tailwindcss";
with automatic source detection, and you add extra folders or inline safelists with the @source
directive. Older projects set a content array in tailwind.config.js. Check
which one your project uses before changing anything.
Conflicting utilities
When a component receives className="p-4" from a parent and already has p-2,
both classes are on the element and the winner is whichever rule comes later in the generated stylesheet,
not whichever comes later in the attribute. That is why overrides seem to work for some utilities and
not others. A merge helper such as tailwind-merge removes the conflicting class, so the one passed in
by the parent reliably wins.
The runtime cost of CSS-in-JS
Runtime CSS-in-JS libraries generate styles while components render. That is flexible, but it has costs that build-time approaches avoid:
- More JavaScript. The library itself ships to the browser, along with the style definitions for every component.
- Work during rendering. Styles are serialised, hashed and inserted as components render, which adds to the work React does on each render, including during interactions.
- Unstyled first paint without server support. If styles are only created in the browser, server-rendered HTML arrives without them and the page appears unstyled until JavaScript runs.
- Friction with Server Components. Libraries that rely on React context and runtime insertion need their components marked as client components, and in the Next.js App Router they need a registry so styles collected on the server are written into the HTML.
Making an existing setup work properly
// next.config.js: keep styled-components class names consistent on server and client
module.exports = {
compiler: {
styledComponents: true
}
};
Pair this with the style registry pattern from the Next.js documentation for your CSS-in-JS library, rendered in the root layout, so styles are collected during server rendering and inserted into the head.
Build-time alternatives
If you are starting fresh or refactoring heavy components, consider an approach that produces static CSS files at build time: CSS Modules, Tailwind, or a zero-runtime library such as vanilla-extract, Linaria, Panda CSS or StyleX. These keep a typed, component-oriented way of writing styles while shipping ordinary cacheable stylesheets. Migrate component by component, starting with those that render most often or appear above the fold.
Stylesheets managed by React
Recent versions of React can manage stylesheet links rendered inside components. A <link rel="stylesheet">
with a precedence attribute is moved into the document head, de-duplicated if several components
render it, and ordered by precedence. React also waits for the stylesheet to load before revealing content
in a Suspense boundary that depends on it, which prevents a flash of unstyled content for that part of
the page.
function MapWidget() {
return (
<>
<link rel="stylesheet" href="/css/map-widget.css" precedence="default" />
<div className="map-widget">...</div>
</>
);
}
This is useful for large, rarely used widgets whose CSS you do not want in the main bundle. Frameworks may handle CSS for you in other ways, so check how your framework expects stylesheets to be included before mixing approaches.
For trimming the CSS that React apps ship, especially leftover global stylesheets and whole component libraries, see Fixing unused CSS and Fixing oversized stylesheets.
1. Why does my React app flash unstyled content?
In development this is normal: the dev server injects CSS through JavaScript so it can hot-reload, and the page briefly renders before the styles arrive. Check a production build before worrying. If it also happens in production, the usual cause is runtime CSS-in-JS without server rendering support, so the HTML arrives without its styles. Configure the library's server-side style collection, or move those components to CSS Modules, Tailwind or another approach that produces static CSS at build time.
2. Why do my styles change depending on which page I visit first?
Bundlers split CSS into chunks per route, and when two rules of equal specificity target the same element, whichever chunk loaded last wins. Landing on a page directly and navigating to it can load chunks in a different order. Keep every rule for a component in that component's own file, import global styles once at the root, and use cascade layers so the order of groups is fixed regardless of which chunk arrives first.
3. Why are some Tailwind classes not working?
Tailwind only creates CSS for class names it finds written in full in the files it scans. Classes assembled from string fragments, stored in a CMS, or living in folders outside the scan are never generated, so they do nothing. Write full class names in a lookup object, add extra source paths with the setting your Tailwind version uses, or safelist names that come from outside the code. Also check for two conflicting utilities on the same element.
4. Is styled-components or Emotion bad for performance?
They are not bad in themselves, but runtime CSS-in-JS does add work that build-time approaches avoid: the library ships to the browser, styles are generated while components render, and without server rendering support the first paint is unstyled. For many apps the cost is acceptable once server rendering is set up correctly. For new projects, or components that render very often, CSS Modules, Tailwind or a zero-runtime library usually give the same developer experience with less runtime cost.
5. Can I use CSS-in-JS with the Next.js App Router?
Yes, with some setup. Runtime libraries such as styled-components and Emotion rely on client-side React features, so components using them must be client components. To avoid unstyled pages, add a style registry in the root layout that collects styles during server rendering and inserts them into the HTML, following the Next.js documentation for your library. Server Components themselves cannot use these libraries directly, which is one reason many teams move to build-time styling.
6. Should I use CSS Modules or Tailwind in React?
Both produce static CSS at build time and work well with server rendering, so the choice is mostly about how your team likes to write styles. CSS Modules keep ordinary CSS in a file beside each component with locally scoped class names. Tailwind composes designs from utility classes in the markup and generates only the classes you use. Many projects combine them, using Tailwind for layout and spacing and modules for complex component styles. Pick one primary approach and stay consistent.
7. How do I set a dynamic colour or width without inline styles everywhere?
Set a CSS custom property through the style prop and read it from a normal stylesheet rule. For example, pass the progress value as a property named progress on the element, and write width: var(--progress) in the component's CSS Module. React only updates one property when data changes, the stylesheet stays static and cacheable, and you avoid generating a new class or style tag for every possible value, which is what runtime CSS-in-JS libraries otherwise do.
The CSS Checker measures the stylesheets your pages load, flags render-blocking and unused CSS, validates against W3C standards and scans for specificity issues and prefix gaps, so you can see what each route really delivers.
Open CSS Checker Fixes โ