---
title: Installation
description: "Set up the StyleX build and install your first components — Next.js and Vite."
sidebar:
  order: 1
---

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.

## With the CLI (recommended)

```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](#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`:

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

```js
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`](https://github.com/madeui/ui/blob/main/packages/cli/src/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:

```ts
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`](https://github.com/madeui/ui/blob/main/packages/cli/src/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](/docs/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

- [Customization](/docs/customization) — tokens, themes, the `style` prop.
- [Dark mode](/docs/dark-mode) — follow the OS or add a toggle, without a flash.
- [CLI reference](/docs/cli) — everything `madeui init`, `add`, and `list` do.
