Installation
Set up the StyleX build and install your first components — Next.js and Vite.
Components are copied into your project as source, so your app needs to be
able to compile StyleX — that’s the only setup beyond a normal React app.
The madeui init command does it for you; the manual steps are below it for
reference.
Requirements
- React 19 (and
react-dom19). Base UI and StyleX themselves support older React, but the components are written and tested against 19. - Node 22.12 or newer for the CLI and the StyleX build.
- Next.js 15 or newer with the App Router. Turbopack (the default in
Next 16) works: StyleX compiles inside the PostCSS step, and the
babel.config.jsinitwrites is only read by that step. - Vite with
@vitejs/plugin-react(v6 tested). - TypeScript. Components ship as
.tsx.
Tailwind and StyleX don’t mix in one build; init offers to remove Tailwind
from an app that was scaffolded with it.
With the CLI (recommended)
npx @madeui/cli init
npx @madeui/cli add button dialog selectinit detects your framework (Next.js or Vite), wires the StyleX build,
installs @stylexjs/stylex + @base-ui/react, copies the design tokens to
lib/, applies the page styles in your root layout, and
appends a component-conventions section to AGENTS.md so coding agents
follow the house rules. add copies components (and anything they
depend on) into components/ui/.
If the app was scaffolded with Tailwind, init offers to remove it — madeui
uses StyleX and the two don’t mix well in one build. Pass --remove-tailwind
or --keep-tailwind to skip the question.
Next.js (manual)
StyleX compiles through Babel; on Next.js it runs inside PostCSS via the
official @stylexjs/postcss-plugin.
npm install @stylexjs/stylex @base-ui/react
npm install -D @stylexjs/babel-plugin @stylexjs/postcss-pluginbabel.config.js:
const path = require('path');
// Project root. Not __dirname: when Turbopack loads postcss.config.mjs it
// bundles this file too, and __dirname becomes the virtual '/ROOT/'.
const root = process.cwd();
module.exports = {
presets: ['next/babel'],
plugins: [
[
'@stylexjs/babel-plugin',
{
dev: process.env.NODE_ENV !== 'production',
runtimeInjection: false,
treeshakeCompensation: true,
aliases: { '@/*': [path.join(root, '*')] },
unstable_moduleResolution: { type: 'commonJS' },
},
],
],
};postcss.config.mjs (ESM, like the file create-next-app writes; the Babel
config above has to stay a CommonJS .js file because Next’s Babel loader
does not load .mjs):
import babelConfig from './babel.config.js';
const config = {
plugins: {
'@stylexjs/postcss-plugin': {
include: [
'app/**/*.{js,jsx,ts,tsx}',
'components/**/*.{js,jsx,ts,tsx}',
'lib/**/*.{js,jsx,ts,tsx}',
],
babelConfig: {
babelrc: false,
parserOpts: { plugins: ['typescript', 'jsx'] },
plugins: babelConfig.plugins,
},
// StyleX declares the layer order itself: base, then its own layers.
useCSSLayers: { before: ['base'] },
},
},
};
export default config;app/globals.css — the @stylex marker is where the generated CSS lands.
Keep global CSS in @layer base, declared before the marker: with
useCSSLayers, any unlayered CSS outranks every StyleX rule and silently
zeroes component paddings. Start the file with the layer statement, the
marker, and a browser reset in that layer. init writes
reset.css,
which has the same coverage as Tailwind Preflight (border-box sizing, zero
user-agent margins, form controls inheriting fonts, and so on), so an app
moving off Tailwind keeps the baseline it rendered under. Copy it, or write
your own inside @layer base:
@layer base;
@stylex;
/* madeui reset */
@layer base {
/* contents of reset.css */
}
@layer base {
/* your own global styles */
}Vite (manual)
Vite uses the official @stylexjs/unplugin — it runs its own Babel pass
(works with @vitejs/plugin-react v6, which dropped its Babel pipeline) and
injects the extracted CSS into the app’s CSS asset. No PostCSS config or
@stylex marker needed.
npm install @stylexjs/stylex @base-ui/react
npm install -D @stylexjs/unpluginvite.config.ts — the StyleX plugin must come before the React plugin to
preserve Fast Refresh:
import { fileURLToPath } from 'node:url';
import { unplugin as stylexPlugin } from '@stylexjs/unplugin';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
stylexPlugin.vite({
aliases: {
'@/*': [fileURLToPath(new URL('src/*', import.meta.url))],
},
unstable_moduleResolution: { type: 'commonJS' },
}),
react(),
],
resolve: {
alias: { '@': fileURLToPath(new URL('src', import.meta.url)) },
},
});Make sure the app imports at least one CSS file from its root (e.g.
import './index.css' in main.tsx) — the plugin appends the generated CSS
to the app’s CSS bundle.
src/index.css — add the same
reset.css
block init writes (the Vite template has no box-sizing: border-box rule,
so inputs and buttons would grow by their borders). The StyleX output is
unlayered on Vite, so the @layer base reset always loses to component
styles.
Page styles
lib/themes.ts exports two styles for the page itself: colorScheme for
<html>, which switches every color token between light and dark (see
Dark mode), and page for <body>, which sets the
background and text color from the background and foreground tokens.
init applies both; to do it by hand on Next.js:
// app/layout.tsx
import * as stylex from '@stylexjs/stylex';
import { colorScheme, page } from '@/lib/themes';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" {...stylex.props(colorScheme)}>
<body {...stylex.props(page)}>{children}</body>
</html>
);
}When <html> already has a className (next/font variables, say), append
the StyleX class to it instead of spreading, since an element takes one
className:
<html lang="en" className={`${geist.variable} ${stylex.props(colorScheme).className}`}>page goes on <body>: while <html> has no background of its own, the
body background fills the whole window, and a StyleX class on <body> wins
over body { background: … } rules in @layer base, below every StyleX
rule. Starter stylesheets often set background or color on :root,
html, or body outside any layer (create-vite’s index.css does); delete
those rules so the tokens decide the page colors.
On Vite, <html> and <body> live in index.html, outside React, so add
the classes once from the entry:
// src/main.tsx
import * as stylex from '@stylexjs/stylex';
import { colorScheme, page } from './lib/themes';
document.documentElement.classList.add(...(stylex.props(colorScheme).className?.split(' ') ?? []));
document.body.classList.add(...(stylex.props(page).className?.split(' ') ?? []));Path aliases
Components import tokens and utilities via @/lib/... and each other via
@/components/ui/.... Add the alias to tsconfig.json if you don’t have it:
{
"compilerOptions": {
"paths": { "@/*": ["./*"] }
}
}(For Vite projects the alias usually points at ./src/*.)
Next steps
- Customization — tokens, themes, the
styleprop. - Dark mode — follow the OS or add a toggle, without a flash.
- CLI reference — everything
madeui init,add, andlistdo.