Library / SDK
kriasoft/isomorphic-style-loader avatar
kriasoft/isomorphic-style-loader

isomorphic-style-loader collects the CSS your server actually rendered

CSS style loader for Webpack that is optimized for isomorphic (universal) web apps.

1,273 stars141 forksJavaScriptMIT

At a glance

What is it?
isomorphic-style-loader is a MIT licensed Webpack loader for React server side rendering, designed so the CSS of the components that were actually rendered is collected into the head of the server's HTML and the rest is left to the client, which avoids a flash of unstyled content without giving up server rendering.
Who is it for?
isomorphic-style-loader suits a team that renders React on the server and wants the first paint to include the styles for what was rendered, without giving up the per component styling model of CSS Modules. It does not suit a project that renders a static page and links one stylesheet, where the whole question of which CSS is critical does not arise, and it is the wrong choice if your React version is below 16.8 or if you are still on Webpack 4.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 40 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is a flash of unstyled content on a server rendered page

isomorphic-style-loader is a CSS style loader for Webpack that works similarly to style-loader, but is optimized for critical path CSS rendering and also works well in the context of isomorphic apps, which is the same thing as universal web apps in the language the README uses.

The problem it addresses is specific to server side rendering. When a React application is rendered on the server, the HTML arrives with the markup but not with the styles, because the styles live in the JavaScript bundle. The browser then has to download and execute that bundle before it can apply the CSS, and until it does, the user sees unstyled markup. That is the flash of unstyled content the README links to, and it is the reason a server rendered page can be slower to look ready than the same page rendered entirely in the browser.

The usual answers are unappealing for this case. Extracting the CSS into a separate file and linking it in the head works, but it means every stylesheet is downloaded on every page whether or not the page needs it. And the loader it resembles, style-loader, injects CSS into the DOM at runtime, which does nothing for the server's response because the server never runs the browser's DOM.

This loader takes the middle path. Instead of only injecting styles into the browser, it keeps the CSS as a string that the application can collect while it renders, so the server can put exactly the styles for the components it rendered into the head of the HTML it sends.

Two methods on the styles object do the work

The entire API surface is two methods on the styles object that the loader generates, and the README names them directly.

The first is ._insertCss(), which injects the CSS into the DOM. That is the client side behaviour, and it is what a browser needs once the page becomes interactive. The second is ._getCss(), which returns a CSS string. That is the server side behaviour, and it is what lets a server collect styles while it renders a request.

Two methods with an underscore prefix is a deliberate signal that they are the loader's own contract with the application rather than a public API for general use, and it also tells you the shape of the integration. The application is expected to call one method or the other depending on where it is running, and the decision is made by the application, not by the loader.

That decision is where the React context comes in. The README's server side rendering example passes an insertCss function through a context variable, and the function it passes is the one that calls styles._getCss() on the server. On the client, the same context carries a function that calls styles._insertCss() and returns the dispose functions, so the styles can be removed again when a component unmounts.

The consequence is that the same component code, the same withStyles decorated component, works in both environments without knowing which one it is in. That is the whole point of an isomorphic loader, and it is why the loader needs a context rather than a global.

The webpack wiring is identical for the client and the server

The webpack configuration puts the loader ahead of css-loader and postcss-loader in the chain, and the note in the README says that the configuration is the same for both client side and server side bundles.

js
use: [
  'isomorphic-style-loader',
  {
    loader: 'css-loader',
    options: {
      importLoaders: 1,
      esModule: false
    }
  },
  'postcss-loader'
]

The requirements are worth stating precisely, because two of them are easy to miss. Webpack 5 is required, and React 16.8 or newer is required, which is the version that introduced hooks, since the hook based API is part of the library.

The third requirement is the subtle one: css-loader must be configured with esModule: false, because the loader reads the CSS module class names from exports.locals. With the default ES module output from css-loader, those class names are not where this loader expects them, and the mapping from the generated class names to the styles it can produce is empty. Nothing crashes, the styles simply are not there, which is the sort of failure that costs an afternoon.

The other option, getCss, carries a constraint that is stated in the same breath as the requirements. The optional getCss loader option is serialized into the generated module, so it must be a self-contained function without closed-over variables. That is a build-time restriction rather than a runtime one, and it means a getCss implementation cannot close over anything from the module it is written in.

The server collects styles, the client hydrates and disposes

The server side rendering example in the README shows the whole handshake, and it is the clearest description of how the library is used.

The server keeps a Set of CSS strings for the request, and the insertCss function it passes through the context is a one liner that adds each style's _getCss() result to that Set:

js
const css = new Set() // CSS for all rendered React components
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()))

Because it is a Set, the same stylesheet added by two different components is stored once, which is a detail that matters as soon as a page renders more than one component using the same module.

The server then renders the application with ReactDOM.renderToString inside a StyleContext.Provider, and writes the collected CSS into a style element in the head of the HTML it sends. The client picks up from there by calling ReactDOM.hydrate with the same provider, and its insertCss implementation calls styles._insertCss() for each style and collects the returned dispose functions, so that the cleanup can happen when the component unmounts.

That is the complete contract, and it explains the design choice of having two methods rather than one. The server needs the text, the client needs the injection and the teardown, and the application chooses which one it is by which function it puts in the context.

Only the CSS of what was rendered reaches the head

The README makes a claim about the output that is worth reading precisely, because it is the payoff of the whole arrangement.

Regardless of how many style components there are in the app.js bundle, only critical CSS is going to be rendered on the server inside the head section of the HTML document. Critical CSS is what is actually used on the requested web page, effectively dealing with the flash of unstyled content issue and improving client side performance. And the CSS of the unmounted components will be removed from the DOM.

That last sentence is the part that distinguishes this from a plain extraction plugin. The set of styles the server sends is determined by what the render actually touched, not by what the bundle contains. A component that was never rendered because the route was a different one contributes nothing to the head.

The example output in the README shows the shape, with the generated class names in the style element:

html
<style type="text/css">
  .App_root_Hi8 { padding: 10px }
  .App_title_e9Q { color: red }
</style>

Those hashed class names are the CSS Modules convention, which is why the README says the library works great with CSS Modules, and it is the reason the whole design is coherent. The component's markup uses s.root and s.title, the loader produces the hashed names, and the server sends exactly the rules for the names that appear in the markup it just produced.

The cost of that coherence is that the styles must be reachable at render time, which is why the insertCss function is threaded through the context from the top of the tree rather than being imported inside a component.

withStyles, useStyles, and the constraints of each

There are two ways to use the library, and the newer one is the hook.

The original is the withStyles higher-order component, imported from isomorphic-style-loader/withStyles, which decorates a component with its styles:

js
import React from 'react'
import withStyles from 'isomorphic-style-loader/withStyles'
import s from './App.scss'

function App(props, context) {
  return (
    <div className={s.root}>
      <h1 className={s.title}>Hello, world!</h1>
    </div>
  )
}

export default withStyles(s)(App) // <--

The hook version, imported from isomorphic-style-loader/useStyles, can be used inside a React function component instead of using withStyles. The README notes that you still need to pass the insertCss function to StyleContext.Provider from the top of the tree, which is the same requirement as the higher-order component version. The context is not optional in either case.

The package metadata shows the export map, which lists the four entry points a consumer uses, the main index and then ./withStyles, ./useStyles, ./StyleContext and ./insertCss, each with a .js variant for compatibility. The peer dependencies are React and React DOM, both accepting 16.8, 17, 18 and 19.

One design constraint is easy to miss when you read the requirements: the optional getCss loader option is serialized into the generated module, so it must be a self-contained function without closed-over variables. Everything else in the API can be written normally, but a getCss function is compiled into the bundle as source, which rules out capturing anything from the surrounding scope.

Version 6 changed what the wrapped component exposes

The breaking changes in v6 are listed under their own heading, and both of them are about the shape of the wrapped component rather than about CSS.

The first is that static properties of the wrapped component, for example fetchData, are no longer hoisted onto the component returned by withStyles. You access them via the ComposedComponent property instead. For an application that decorates a container component with a data loading method, this is the difference between the method being on the exported component and the method being reachable one level in, and a code change wherever the static was used.

The second is that prop-types validation has been removed. That is consistent with the peer dependency range, which no longer includes the prop-types package at all, and it means the library no longer validates the props of the components it wraps.

On the release line, the tags are v6.0.0 from 2026-06-05, v5.4.0 from 2024-12-04 and v5.3.2 from 2021-08-26. The repository is not archived and the last push was on 2026-09-13, so the project is being worked on, but the gap between v5.3.2 and v5.4.0 shows a project that releases when there is a reason rather than on a schedule.

The licence is MIT, held by Kriasoft, and the repository is a small, well organised package: src/, test/, tools/, a .babelrc, a .nvmrc, a .prettierrc, an eslint.config.mjs, husky for the git hooks and a CONTRIBUTING.md. The related projects listed at the end of the README are the React Starter Kit, an isomorphic web app boilerplate using Express.js, React and Relay, and the Node.js API Starter, a template for GraphQL API backends, which tells you the kind of applications this loader was built for.

Editorial conclusion

isomorphic-style-loader suits a team that renders React on the server and wants the first paint to include the styles for what was rendered, without giving up the per component styling model of CSS Modules. It does not suit a project that renders a static page and links one stylesheet, where the whole question of which CSS is critical does not arise, and it is the wrong choice if your React version is below 16.8 or if you are still on Webpack 4. Verify first that css-loader is configured with esModule false, because the loader reads the class names from exports.locals and will not find them otherwise, and check the v6 breaking changes, since static properties of the wrapped component are no longer hoisted onto the returned component.

Frequently asked questions

What does isomorphic-style-loader do differently from a normal style loader?

It works similarly to style-loader but is optimized for critical path CSS rendering and for isomorphic apps, providing two helper methods on the styles object, ._insertCss() to inject CSS into the DOM and ._getCss() to return a CSS string that a server can collect while rendering.

How do I install isomorphic-style-loader?

With npm install isomorphic-style-loader --save-dev. It requires Webpack 5 and React 16.8 or newer, and css-loader must be configured with esModule: false because the loader reads the CSS module class names from exports.locals.

How do I use it with server side rendering?

Pass an insertCss function through StyleContext.Provider that calls styles._getCss() on each style and collects the results into a set, then write that CSS into a style element in the head of the HTML. On the client, hydrate with a context whose insertCss calls styles._insertCss() and returns the dispose functions.

What changed in isomorphic-style-loader v6?

Two breaking changes: static properties of the wrapped component are no longer hoisted onto the component returned by withStyles and must be accessed through the ComposedComponent property, and prop-types validation has been removed.

Can I use hooks instead of the withStyles component?

Yes. You can call useStyles inside a React function component instead of using withStyles, importing it from isomorphic-style-loader/useStyles. You still need to pass the insertCss function to StyleContext.Provider from the top of the tree.

Official sources

  1. kriasoft/isomorphic-style-loader on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kriasoft-isomorphic-style-loader.svg)](https://hysenlabs.com/projects/kriasoft-isomorphic-style-loader)

Was this useful?