@stylexswc/unplugin
v0.19.0
Published
Universal StyleX plugin for Vite, webpack, Rspack, Rollup, esbuild, Farm, Rsbuild, Nuxt, and Astro, powered by a Rust NAPI-RS/SWC compiler.
Maintainers
Readme
@stylexswc/unplugin
Universal StyleX plugin for Vite, webpack, Rspack, Rollup, esbuild, Farm, Rsbuild, Nuxt, and Astro — powered by a Rust compiler (NAPI-RS + SWC). Part of the StyleX SWC Plugin workspace.
Built on unplugin, this package gives every major
bundler the same StyleX integration: it compiles your
StyleX code with
@stylexswc/rs-compiler,
a Rust implementation of the StyleX transform, and extracts the generated CSS.
Your StyleX code stays exactly the same — only the build step changes, with
per-file transforms 5x to 10x faster than Babel
(performance).
This is a community project and is not affiliated with Meta. It tracks the official StyleX releases
and requires Node.js 20 or newer.
Installation
npm install --save-dev @stylexswc/unpluginThe Rust compiler (@stylexswc/rs-compiler) is installed automatically as a
dependency. Your application still needs the StyleX runtime:
npm install @stylexjs/stylexUsage
Import the entry point matching your build tool and add it to the plugin list. A
working example for each bundler lives in the
apps/{pluginName}-unplugin-example
folders.
// vite.config.ts
import StylexRsPlugin from '@stylexswc/unplugin/vite';
export default defineConfig({
plugins: [StylexRsPlugin({/* options */})],
});// rollup.config.js
import StylexRsPlugin from '@stylexswc/unplugin/rollup';
export default {
plugins: [StylexRsPlugin({/* options */})],
};// webpack.config.js
module.exports = {
/* ... */
plugins: [require('@stylexswc/unplugin/webpack')({/* options */})],
};// rspack.config.js
module.exports = {
/* ... */
plugins: [require('@stylexswc/unplugin/rspack')({/* options */})],
};// nuxt.config.js
export default defineNuxtConfig({
modules: [['@stylexswc/unplugin/nuxt', {/* options */}]],
});This module works for both Nuxt 2 and Nuxt Vite
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [require('@stylexswc/unplugin/webpack')({/* options */})],
},
};// esbuild.config.js
import { build } from 'esbuild';
import StylexRsPlugin from '@stylexswc/unplugin/esbuild';
build({
plugins: [StylexRsPlugin()],
});Plugin Options
Basic Options
rsOptions
- Type:
Partial<StyleXOptions> - Optional
- Description: StyleX compiler options passed to
@stylexswc/rs-compiler. For the standard options, see the official StyleX documentation.
[!NOTE] The
includeandexcludeoptions are exclusive to the Rust compiler and are not available in the official StyleX Babel plugin.
rsOptions.include
- Type:
(string | RegExp)[] - Optional
- Description: Glob patterns or regular expressions selecting the files to transform. When specified, only files matching at least one pattern are transformed. Patterns are matched against paths relative to the current working directory.
rsOptions.exclude
- Type:
(string | RegExp)[] - Optional
- Description: Glob patterns or regular expressions excluding files from the
transform. A file matching any exclude pattern is skipped even if it matches
an
includepattern. Patterns are matched against paths relative to the current working directory.
fileName
- Type:
string - Default:
'stylex.css' - Description: Name of the generated CSS file.
useCSSLayers
- Type:
UseLayersType - Default:
false - Description: Wraps the generated CSS in cascade layers for better style isolation.
extractCSS
- Type:
boolean - Default:
true - Description: Controls whether the generated CSS is extracted into a separate file.
pageExtensions
- Type:
string[] - Default:
['js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'mts', 'cts'] - Description: File extensions to process for StyleX transformations. The default holds every extension that the compiler transforms. A shorter list leaves StyleX in the other extensions uncompiled.
transformCss
- Type:
CSSTransformer - Optional
- Description: Transforms the extracted StyleX CSS before it is emitted or
injected. Matches the
@stylexswc/webpack-pluginAPI. Use it to run the generated CSS through PostCSS, Lightning CSS, a minifier, or any custom post-processing step.
type CSSTransformer = (
css: string,
filePath: string | undefined
) => string | Buffer | Promise<string | Buffer>;import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
StylexRsPlugin({
async transformCss(css) {
const result = await postcss([autoprefixer]).process(css, {
from: undefined,
});
return result.css;
},
});The filePath argument identifies the CSS destination and is bundler-specific:
- webpack/rspack/rollup injection: the output asset name (e.g.
app.css) - esbuild disk writes: the absolute path of the written file
- Vite placeholder replacement: the id of the CSS module being loaded
- generated assets: the configured
fileName, with[hash]left unresolved (the hash is computed from the transformed CSS, so it cannot be known earlier) undefinedwhen no destination is known
[!NOTE]
Bufferresults are decoded as UTF-8. Results are memoized perfilePathwhile the input CSS is unchanged, so the callback must be a pure function of its arguments — the same input may be served from cache instead of invoking the callback again.
useCssPlaceholder
- Type:
boolean | string - Default:
false - Description: Injects the generated CSS into an existing CSS file via a
placeholder marker.
- When set to
true, the plugin looks for the default@stylex;marker - When set to a string, the plugin uses that string as the custom marker
- When set to
Routing the StyleX output through a real CSS file has practical benefits:
- Your stylesheet keeps going through the bundler's CSS pipeline (PostCSS, Lightning CSS, css-loader, and so on), and the StyleX rules land inside it at the marker
- Deterministic builds — no race conditions or hash instability from virtual modules
- One stylesheet, so the rules follow the same bundling strategy as the rest of your CSS
- CSS can be code-split and optimized alongside other stylesheets
- The same approach works for Vite, webpack, Rspack, esbuild, and Rollup
How to use it:
- Create a CSS file with a marker (e.g.
global.css):
/* global.css */
:root {
--brand-color: #663399;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
}
@stylex;- Import the CSS file in your entry point:
// src/main.ts
import './global.css';
import { App } from './App';- Configure the plugin with
useCssPlaceholder:
// vite.config.ts (or webpack.config.js, rspack.config.js, etc.)
import StylexRsPlugin from '@stylexswc/unplugin/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
StylexRsPlugin({
useCssPlaceholder: true, // Uses default '@stylex;' marker
useCSSLayers: true,
}),
],
});Or with a custom marker string:
/* global.css */
:root {
--brand-color: #663399;
}
/* INJECT_STYLEX_HERE */StylexRsPlugin({
useCssPlaceholder: '/* INJECT_STYLEX_HERE */',
useCSSLayers: true,
});The plugin replaces the marker with the generated StyleX CSS during the build.
[!NOTE] When
useCssPlaceholderis enabled, the plugin no longer injects CSS automatically into HTML or emits a separatestylex.cssfile. The CSS goes into your specified CSS file instead. No bundler emits a standalone stylesheet in this mode any more, since nothing would link it: under Vite, where the plugin can tell the marker was part of the build, a missing injection target fails the build; every other bundler reports a warning.A plugin that removes CSS assets during the bundle — inlining them into JS, for instance — takes that target away, so combining one with
useCssPlaceholderunder Vite fails the build. SetonMissingCssPlaceholderto'warn'when that is expected.If a stylesheet survives but something strips the marker out of it, the rules are appended to the end of a stylesheet instead of landing at the marker, and the plugin warns. The styles still ship; only their position is lost.
fileNamedoes nothing in this mode: no standalone stylesheet is emitted, and your own keeps the name the bundler gives it.
[!IMPORTANT] Stylesheet names change with the StyleX rules
The rules go in after the bundler has hashed the stylesheet, so the plugin renames it to match the new contents. Without this, a cache can serve old CSS to new JavaScript. Upgrading renames every placeholder stylesheet once.
- Plugins that read CSS file names from the bundle must run after this one. Documents and Vite manifests are updated in place; data outside the bundle, such as a service-worker precache list, is not.
- Under Vite, set
build.cssCodeSplit: false. The renaming does not apply to the defaulttrue.- The plugin does not rename when the name template has no hash (
assetFileNames,entryNames), or whenassetFileNamesis a function.- webpack and Rspack rename the asset themselves. Keep
optimization.realContentHashon, which is the production default.
[!IMPORTANT] What the CSS pipeline does and does not see
Your stylesheet goes through the pipeline in full. The StyleX rules do not: in a build they are spliced in at the marker once the bundle is assembled, because modules behind a dynamic import are transformed long after the stylesheet is loaded, and there is no earlier point where the rule set is complete.
So the rules are minified along with everything else, but per-module steps such as PostCSS or Lightning CSS transpilation never run over them. Use
transformCssto run your own processing over the rules. In development the rules are put into the stylesheet before the pipeline runs, so there it sees everything. The bundled mode below uses Vite's built-in CSS preprocessing, not other plugins' CSS transform hooks.
[!NOTE] Vite's bundled dev server (
experimental.bundledDev)The plugin serves the marker stylesheet with current rules and Vite's built-in CSS preprocessing (PostCSS, Lightning CSS and
@import). Edits to StyleX modules, shared variables and themes, the stylesheet and its imports refetch the stylesheet over HMR. Rolldown decides whether the module edit itself is a hot update or a reload.Limits in this mode:
- The marker must be in a plain stylesheet that JavaScript imports. A CSS module or a build entry gets only the rules known at bundle time.
- Restart after adding or removing a marker or its stylesheet import.
- Other Vite plugins' CSS transform hooks do not run over this stylesheet.
url()references are not rewritten or served. Keep those assets inpublic/or in a stylesheet without the marker.
[!WARNING] Farm does not support
useCssPlaceholderyet. Its plugin adapter never receives the bundle hook that replaces the marker, so the marker would stay in the stylesheet. The plugin warns when the two are combined.
[!IMPORTANT] Migration from
useViteCssPipelineThe
useViteCssPipelineoption (which used virtual CSS modules) has been replaced byuseCssPlaceholder. The new approach uses real CSS files instead of virtual modules, which provides better compatibility across all bundlers, no race conditions or timing issues, and deterministic builds with stable hashes. To migrate, create a CSS file with a marker and setuseCssPlaceholder: true(or use a custom marker string).
onMissingCssPlaceholder
- Type:
'error' | 'warn' | 'ignore' - Default:
'error' - Description: How to report a build where
useCssPlaceholderis enabled but no stylesheet in the output can carry the StyleX rules.
Placeholder mode never links a standalone stylesheet, so the default fails the
build rather than let the styles go missing at runtime. Only Vite can prove the
marker was part of the build, so 'error' is fatal there and reported as a
warning everywhere else.
StylexRsPlugin({
useCssPlaceholder: true,
// Another plugin inlines the CSS into JS, so there is no CSS asset left to
// inject into by the time the rules are ready.
onMissingCssPlaceholder: 'warn',
});'ignore' silences the report entirely. SSR builds are never reported: they
have no stylesheet of their own by design.
Example Configuration
// vite.config.ts
import StylexRsPlugin from '@stylexswc/unplugin/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
StylexRsPlugin({
rsOptions: {
dev: process.env.NODE_ENV !== 'production',
include: ['src/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
exclude: ['**/*.test.*', '**/*.stories.*', '**/__tests__/**'],
},
useCSSLayers: true,
useCssPlaceholder: true,
}),
],
});Path Filtering Examples
Include only specific directories:
StylexRsPlugin({
rsOptions: {
include: ['src/**/*.tsx', 'app/**/*.tsx'],
},
});Exclude test and build files:
StylexRsPlugin({
rsOptions: {
exclude: ['**/*.test.*', '**/*.spec.*', '**/dist/**'],
},
});Exclude node_modules except specific packages (negative lookahead):
StylexRsPlugin({
rsOptions: {
exclude: [/node_modules(?!\/@stylexjs)/],
},
});Transform only specific packages from node_modules:
StylexRsPlugin({
rsOptions: {
include: [
'src/**/*.{ts,tsx}',
'node_modules/@stylexjs/open-props/**/*.js',
'node_modules/@my-org/design-system/**/*.js',
],
exclude: ['**/*.test.*'],
},
});Combined include and exclude (exclude takes precedence):
StylexRsPlugin({
rsOptions: {
include: ['src/**/*.{ts,tsx}'],
exclude: ['**/__tests__/**', '**/__mocks__/**'],
},
});FAQ
Should I use this package or the bundler-specific plugin?
If you are on Vite, esbuild, Farm, Rsbuild, Nuxt, or Astro, this is the package
to use. For webpack and Rspack, the dedicated @stylexswc/webpack-plugin and
@stylexswc/rspack-plugin packages offer a few deeper integrations (loader
ordering, cache groups); this plugin covers the common cases with one consistent
API. For Next.js, use @stylexswc/nextjs-plugin.
Do I still need @stylexjs/babel-plugin?
No. This plugin replaces the Babel plugin in your build. You only keep
@stylexjs/stylex as your app's runtime dependency, and your stylex.create /
stylex.props code does not change.
How do I run the generated CSS through PostCSS or Tailwind pipelines?
Either pass a transformCss function, or enable useCssPlaceholder so the
generated CSS lands in a real CSS file and flows through your bundler's normal
CSS pipeline.
Does hot module replacement work?
Yes. The plugin participates in each bundler's standard transform pipeline, so style changes update through the dev server like any other module.
Is this an official StyleX package?
No. It is a community-maintained alternative to the official tooling and is not affiliated with or supported by Meta.
Documentation
License
MIT — see LICENSE
