Skip to content

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-dom 19). 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.js init writes 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.

Bash
npx @madeui/cli init
npx @madeui/cli add button dialog select

init 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.

Bash
npm install @stylexjs/stylex @base-ui/react
npm install -D @stylexjs/babel-plugin @stylexjs/postcss-plugin

babel.config.js:

JavaScript
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):

JavaScript
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:

CSS
@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.

Bash
npm install @stylexjs/stylex @base-ui/react
npm install -D @stylexjs/unplugin

vite.config.ts — the StyleX plugin must come before the React plugin to preserve Fast Refresh:

TypeScript
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:

TSX
// 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:

TSX
<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:

TSX
// 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:

JSON
{
  "compilerOptions": {
    "paths": { "@/*": ["./*"] }
  }
}

(For Vite projects the alias usually points at ./src/*.)

Next steps

Was this page helpful?