Skip to main content
Nexus Design System
GitHub

Install

Set up a React app with nxds-cli, then add any component. For a Vue app, follow Install by hand.

Set up your app

npx nxds-cli@latest init

init sets up a Next.js App Router app or a Vite + React app that uses TypeScript and React 18.3 or 19. It does every step in Install by hand:

StepWhat init does
Tailwind 4Installs it, if your app doesn't have it yet
Token CSSCopies it into components/nxds/tailwind/ and adds an nxds CSS entry beside your app
Appearance providerCopies it with the cn helper, then adds it to app/layout.tsx or src/main.tsx
PackagesInstalls @nxds/core, tw-animate-css, clsx and tailwind-merge
ESLint (Vite)Turns off react-refresh/only-export-components for the nxds folder

Your app's own styles stay as they are. init also writes nxds.json, which records the nxds version and the items you installed.

If your app uses src/, nxds goes in src/components/nxds/. To choose another folder, pass --dir.

(Optional) Set your theme

npx nxds-cli@latest init --brand-color "#4f46e5" --density compact --font inter --heading-font lora --icons lucide

Each flag sets one part of your theme. To build the command, pick a theme in the Playground or the theme panel on any docs page, then open Use this theme.

FlagValues
--brand-colorAny CSS color
--surface-tonestone, neutral, zinc (default), slate, gray
--densitytight, compact, default (default), comfortable, relaxed, spacious
--cornerssquare, subtle (default), smooth, round, extra-round
--elevationflat, quiet (default), soft, standard, strong
--strokefine, normal (default), strong
--modelight (default), dark, system
--light-contrast, --dark-contrast0 to 100 (default 50)
--fontsystem (default), geist, inter, figtree, dm-sans, manrope, ibm-plex-sans, lora, playfair-display
--heading-fontui (the UI font, default), or any --font value but system
--code-fontsystem (default), geist-mono, jetbrains-mono
--font-size, --code-font-size8 to 32, in px (default 14 and 12)
--iconsphosphor (default), lucide, tabler, hugeicons, remix

init writes the theme to nxds-theme.ts and records it in nxds.json. It installs the icon package and each font's Fontsource package, and imports the fonts in the nxds CSS entry. Your appearance-state.ts spreads nxdsTheme, so set your own overrides there. Don't edit nxds-theme.ts, since init rewrites it.

To change the theme, run init again. A flag you leave out keeps its value. An empty flag, such as --density=, resets that part to the default. A new --icons rewrites lib/icons.ts in every installed component. A new font replaces the old one's import, and doctor lists the old package to remove.

If an older init wrote your appearance-state.ts, init asks you to spread nxdsTheme there once.

Add a component

npx nxds-cli@latest add button

add copies the component, the components it uses and the packages it needs, from your app's nxds version. Each component page shows its add command.

Copied files are yours to edit. Import them from the nxds folder, such as components/nxds/components/button/button.

Update components

npx nxds-cli@latest diff
npx nxds-cli@latest update

diff lists the files that differ from the latest release. update moves every installed item to it.

update replaces a file you haven't edited. If you edited a file, update merges the release's changes into it. Line endings and trailing spaces don't count as edits.

If you and the release changed the same lines, update marks them like a git conflict. Keep the lines you want, delete the <<<<<<<, ======= and >>>>>>> markers, then run update again. If a newer release comes out before you resolve every conflict, update changes no files, keeps nxds.json on your release and exits 3. To replace an edited file with the release's copy, pass --overwrite.

Use React 18

nxds runs on React 18.3 and React 19. init reads the React your app uses and records it in nxds.json, so add and update copy the files for it.

On React 18, each component that takes a ref is wrapped in forwardRef, because React 18 drops ref from a function component's props. You pass ref the same way on both.

After you upgrade to React 19, run update:

npx nxds-cli@latest update

update switches the files to React 19 and merges the release's changes into files you edited. Until you run it, add stops and doctor reports the mismatch.

To install by hand on React 18, copy the files from the React 18 tab in a component page's Manual steps.

Find setup problems

npx nxds-cli@latest doctor

doctor checks the provider, the CSS entry, the files, the packages and React, and says how to fix each problem.

Options

OptionWhat it does
--dry-runShows what would change, without changing anything
--jsonPrints { ok, exitCode, data }, or { ok, exitCode, error } on failure
--no-installSkips installing npm packages
--cwd <path>Runs in another folder
--registry <url|path>Installs from another registry. NXDS_REGISTRY_URL sets it too

Run npx nxds-cli@latest --help to list every command, or npx nxds-cli@latest add --help for one command's options.

Behind a proxy, set HTTPS_PROXY. The CLI also reads HTTP_PROXY and NO_PROXY.

Exit codeMeaning
0Done
1Error, such as no network or an invalid release
2Usage error, such as an unknown option or item
3Done, with something left for you: conflicts, kept files, manual steps or failed checks

Install by hand

If the CLI doesn't support your app, follow these steps. They're the steps init and add run for you.

Copied components are your files: edit them freely, and paste a newer version to update.

Step 1: Install Tailwind 4

npm install -D tailwindcss @tailwindcss/postcss postcss
// postcss.config.mjs
export default {
  plugins: { '@tailwindcss/postcss': {} },
};

Tailwind 4 builds the nx: classes. These steps use a new Next.js 15 App Router app. If your app already has a UI, follow Add nxds to an existing app.

Remove the @import "tailwindcss" that create-next-app adds to globals.css. The token CSS brings Tailwind's theme and utilities with the nx: prefix. The prefix applies to the whole build, so a second import in the same file doesn't give you unprefixed classes.

Step 2: Add the token CSS

The token CSS defines the colors, spacing and type scale. Copy the seven files from packages/tailwind/ in the nxds repo into components/nxds/tailwind/:

components/nxds/tailwind/
├── nxds.css                 # entry point, imports the other six
├── variables.css
├── typography-utilities.css
├── borderwidth-utilities.css
├── border-color-aliases.css
├── motion-utilities.css
└── spacing-utilities.css

Install the animation utilities that some components use to open and close:

npm install tw-animate-css

Import Tailwind's Preflight, the token CSS and the animation utilities from your CSS entry point:

/* app/globals.css */
@layer theme, base, components, utilities;
@import 'tailwindcss/preflight.css' layer(base);
@import '../components/nxds/tailwind/nxds.css';
@import 'tw-animate-css';

nxds ships no reset, so the page brings Preflight. nxds components need its box-sizing: border-box.

Every Tailwind class in your app now takes the nx: prefix: nx:flex works, flex does not. Only nxds tokens are available, so Tailwind's default colors are gone.

Instead of nx:bg-red-500, use a token class like nx:bg-error-background.

To add your own tokens, put a @theme block in globals.css after the imports. Don't edit nxds.css, because pasting a newer copy replaces it.

Scrollbars and form controls turn dark with the theme. Don't add a color-scheme meta tag. A light dark meta lets the operating system darken a light page.

Step 3: Add the appearance provider

The provider applies the theme and saves the user's choices, like dark mode. Its script runs in <head>, so the page doesn't flash the wrong theme on load.

If you use Vite, follow Set up Vite for this step instead. Steps 1, 2 and 4 are the same on Vite.

Install the theme engine:

npm install @nxds/core

Copy the provider folder into components/nxds/components/appearance/provider/:

components/nxds/components/appearance/provider/
├── index.ts      # NxdsAppearanceProvider, useNxdsAppearance, createNxdsAppearance
├── provider.tsx
├── factory.tsx
├── script.tsx
└── server.ts     # NxdsAppearanceScript (safe to import on the server)

Set your app's starting theme. Copy the default appearance and change the fields you want:

// app/appearance-state.ts
import { DEFAULT_NXDS_APPEARANCE, type NxdsAppearanceState } from '@nxds/core';

export const defaultAppearance = {
  ...DEFAULT_NXDS_APPEARANCE,
  brandColor: '#2563eb',
  surfaceTone: 'slate',
} satisfies NxdsAppearanceState;

brandColor sets the primary color. surfaceTone sets the gray used for backgrounds. For every field, see Appearance.

Add the script and the provider to your root layout:

// app/layout.tsx
import { NxdsAppearanceProvider } from '@/components/nxds/components/appearance/provider';
import { NxdsAppearanceScript } from '@/components/nxds/components/appearance/provider/server';

import { defaultAppearance } from './appearance-state';

import './globals.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <NxdsAppearanceScript
          storageKey="app-appearance"
          defaultState={defaultAppearance}
        />
      </head>
      <body className="nx:bg-background nx:text-foreground nx:typography-body-default">
        <NxdsAppearanceProvider
          storageKey="app-appearance"
          defaultState={defaultAppearance}
        >
          {children}
        </NxdsAppearanceProvider>
      </body>
    </html>
  );
}

nx:typography-body-default gives page text the nxds font and size, which follow the appearance settings.

Give the script and the provider the same storageKey. The provider saves the user's choices under that key, and the script reads them on the next load.

To render the saved theme on the server, see Load the theme from a cookie.

Step 4: Add the cn helper

npm install clsx tailwind-merge

Copy packages/react/src/lib/utils.ts to components/nxds/lib/utils.ts.

cn merges class names and resolves conflicts between nx: classes. That's what lets <Button className="nx:bg-error-background" /> replace the button's own background.

Add your first component

Each component lists the components to install first, the packages to install, and the files to copy. For Button:

Install these components first: Spinner.

npm install @radix-ui/react-slot@^1.2.4 class-variance-authority@^0.7.1
components/button-group/button-group-context.ts
components/button/button.tsx

Paths are relative to packages/react/src/ in the repo. Copy each file to the same path under components/nxds/, and the imports between files work without edits. If a component has its own stylesheet, like Progress, it also lists the @import lines to add to app/globals.css.

Render it:

'use client';

import { Button } from '@/components/nxds/components/button/button';

export default function ButtonDefault() {
  return <Button>Button</Button>;
}

To check dark mode, add a toggle that uses useNxdsAppearance:

// components/mode-toggle.tsx
'use client';

import { useNxdsAppearance } from '@/components/nxds/components/appearance/provider';
import { Button } from '@/components/nxds/components/button/button';

export function ModeToggle() {
  const { state, setState, resolvedMode } = useNxdsAppearance();

  const toggle = () =>
    setState({ ...state, mode: resolvedMode === 'dark' ? 'light' : 'dark' });

  return <Button onClick={toggle}>Mode: {resolvedMode}</Button>;
}

resolvedMode is the mode on screen, so the toggle also works when mode is 'system'.

Check your setup

  • nxds.css is imported from your CSS entry point
  • The provider wraps your app, and the script is in <head>
  • Reloading in dark mode doesn't flash light first
  • The mode you pick is still set after a full reload

If the button has no styles, nxds.css isn't imported (step 2). If it stays light when you toggle, the provider isn't wrapping it (step 3).

If the page flashes on reload, check that the script and the provider use the same storageKey. With different keys, they load different themes.

Set up Vite

On Vite, this section replaces step 3. Install @nxds/core, copy the provider into src/components/nxds/components/appearance/provider/, and create src/appearance-state.ts as in step 3.

Vite doesn't render on the server, so add the theme script to index.html at build time:

// vite.config.ts
import {
  createNxdsAppearanceBootstrapScript,
  createNxdsAppearanceSnapshotFromState,
} from '@nxds/core';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

import { defaultAppearance } from './src/appearance-state';

const STORAGE_KEY = 'app-appearance';
const defaultSnapshot =
  createNxdsAppearanceSnapshotFromState(defaultAppearance);

export default defineConfig({
  plugins: [
    react(),
    {
      name: 'nxds-appearance-bootstrap',
      transformIndexHtml(html) {
        const script = createNxdsAppearanceBootstrapScript({
          storageKey: STORAGE_KEY,
          defaultSnapshot,
        });
        return html.replace('</head>', `<script>${script}</script></head>`);
      },
    },
  ],
});

Wrap your app in the provider, with the same storage key:

// src/main.tsx
import { createRoot } from 'react-dom/client';

import App from './App';
import { defaultAppearance } from './appearance-state';
import { createNxdsAppearance } from './components/nxds/components/appearance/provider';

import './index.css';

const { NxdsAppearanceProvider } = createNxdsAppearance({
  storageKey: 'app-appearance',
  defaultState: defaultAppearance,
});

createRoot(document.getElementById('root')!).render(
  <NxdsAppearanceProvider>
    <App />
  </NxdsAppearanceProvider>
);

Don't apply the theme in a useEffect. Effects run after the first paint, so the page would flash the default theme first.

On Next.js, read the user's saved theme from a cookie. The server then renders the right theme on the first request. Replace the layout from step 3 with this one:

// app/layout.tsx
import {
  createNxdsAppearanceSnapshotFromCookie,
  DEFAULT_COOKIE_KEY,
} from '@nxds/core';
import { cookies } from 'next/headers';

import { NxdsAppearanceProvider } from '@/components/nxds/components/appearance/provider';
import { NxdsAppearanceScript } from '@/components/nxds/components/appearance/provider/server';

import { defaultAppearance } from './appearance-state';

import './globals.css';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const cookieStore = await cookies();
  const snapshot = createNxdsAppearanceSnapshotFromCookie(
    cookieStore.get(DEFAULT_COOKIE_KEY)?.value,
    defaultAppearance
  );

  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <NxdsAppearanceScript
          storageKey="app-appearance"
          defaultState={snapshot.state}
        />
      </head>
      <body className="nx:bg-background nx:text-foreground nx:typography-body-default">
        <NxdsAppearanceProvider
          storageKey="app-appearance"
          cookieWriteKey={DEFAULT_COOKIE_KEY}
          defaultState={snapshot.state}
        >
          {children}
        </NxdsAppearanceProvider>
      </body>
    </html>
  );
}

If the cookie is missing or invalid, createNxdsAppearanceSnapshotFromCookie falls back to defaultAppearance.

If you read the cookie in the root layout, Next.js renders every route on each request.

The provider saves the theme in two places:

PropSaves toRead by
cookieWriteKeyA cookieThe server, on the next request
storageKeylocalStorageThe head script before first paint, then the provider

When both are set, localStorage wins in the browser. To use only the cookie, set storageKey={false} on both the script and the provider.

If your app uses a strict Content Security Policy, pass a nonce: <NxdsAppearanceScript nonce={nonce} />.

Add nxds to an existing app

nxds CSS defines only nx: classes and --nx-* variables, so your app's styles stay as they are. Follow steps 1 to 4 with these four changes:

  1. Give nxds its own CSS entry and import it after your own stylesheet:

    /* app/nxds.css */
    @import '../components/nxds/tailwind/nxds.css';
    @import 'tw-animate-css';
    

    Your globals.css keeps its @import "tailwindcss". Two files make two Tailwind builds, one prefixed and one not.

  2. Keep your app's reset. nxds needs box-sizing: border-box. Tailwind's Preflight, MUI's CssBaseline and Bootstrap's Reboot all set it. If your app has no Tailwind, install it for the nxds entry. init then adds Preflight to app/nxds.css. If your app or library has its own reset, delete the Preflight lines, so your existing pages keep their look.

  3. Put element rules in a cascade layer, your library's and your own, if they aren't in one. nxds classes sit in Tailwind's utilities layer. CSS outside any layer beats every layer, so a reset like button { border-radius: 0 } would override nx:rounded-md. Name your library's layer first, at the top of your first stylesheet:

    /* app/globals.css */
    @layer your-library, properties, theme, base, components, utilities, nx-runtime;
    

    Then load the library's CSS into that layer. Each Integrations page gives the line for its library.

    If you use Vite, put the statement at the top of <head> in index.html instead:

    <!-- index.html -->
    <head>
      <style>
        @layer your-library, properties, theme, base, components, utilities, nx-runtime;
      </style>
    </head>
    

    The first statement on the page sets the order. On Vite, the nxds script's own statement comes before your CSS.

  4. Add the provider and the script as in step 3. nxds owns light and dark mode, and writes the .dark class on <html>. Make your library follow nxds, as each Integrations page shows.

Three global effects remain:

SourceWhat it adds
.darkcolor-scheme: dark, so your scrollbars and form controls turn dark in dark mode
tw-animate-cssenter, exit, accordion-*, collapsible-* and caret-blink keyframes, as in any shadcn app
Tailwind's own outputspin, ping, pulse and bounce keyframes and --tw-* properties, as in any Tailwind 4 app

Style an nxds component with nx: classes. An unprefixed Tailwind class loses to the component's own nx: class, because both sit in the utilities layer and nxds's stylesheet loads later.

Your content inside an nxds component inherits its text color, font and line height.

If your design system is a package, ship the nxds CSS entry from it and end the entry with @source './';. Tailwind skips node_modules, so without that line the package's classes are never generated.

nxds overlays use three layers:

VariableDefaultUsed by
--nx-z-index-modal50Dialog, Sheet, Drawer
--nx-z-index-popover70Popover, Select, menus, Tooltip
--nx-z-index-toast100Toaster

If your library's overlays sit higher, raise these on :root.

An open nxds dialog blocks pointer events outside itself. Render another library's popups inside it, with portals turned off. Each Integrations page gives its library's z-index values and portal prop.

Examples shows nxds added to a shadcn app, an MUI app and a company package.

(Optional) Add lint rules

The nxds ESLint rules keep copied components following nxds conventions:

npm install -D @nxds/eslint-plugin
// eslint.config.mjs
import { nxdsComponentConfig } from '@nxds/eslint-plugin/config';

const eslintConfig = [
  // ...your existing config
  {
    files: ['components/**/*.tsx'],
    ...nxdsComponentConfig(),
  },
];

export default eslintConfig;

nxdsComponentConfig turns on three rules for the files you list:

RuleCatches
nx-class-conventionsWrong prefix order, removed tokens, incomplete token names and raw palette colors
no-render-prop-typesProps that inject what a component renders, instead of children
no-multi-statement-jsx-handlerLong inline event handlers that should be named functions

The rules need a TypeScript parser. create-next-app sets one up through next/core-web-vitals. If your config has none, install typescript-eslint and add languageOptions: { parser: tseslint.parser } to the entry.

Where the files are

Every file you copy comes from the nxds repo:

FilePath in the repoPath in your app
Token CSSpackages/tailwind/*.csscomponents/nxds/tailwind/
Appearance providerpackages/react/src/components/appearance/provider/components/nxds/components/appearance/provider/
cn helperpackages/react/src/lib/utils.tscomponents/nxds/lib/utils.ts
Componentspackages/react/src/components/{name}/components/nxds/components/{name}/

Step 1: Install Tailwind 4

npm install -D tailwindcss @tailwindcss/vite

Tailwind 4 builds the nx: classes. These steps use a new Vite + Vue 3 app from npm create vue@latest, with TypeScript.

Step 2: Add the token CSS

The token CSS defines the colors, spacing and type scale. Copy the seven files from packages/tailwind/ in the nxds repo into src/styles/nxds/.

Install the animation utilities that some components use to open and close:

npm install tw-animate-css

Import Tailwind's Preflight, the token CSS and the animation utilities from your CSS entry point:

/* src/style.css */
@layer theme, base, components, utilities;
@import 'tailwindcss/preflight.css' layer(base);
@import './styles/nxds/nxds.css';
@import 'tw-animate-css';

nxds ships no reset, so the page brings Preflight. nxds components need its box-sizing: border-box.

Every Tailwind class in your app now takes the nx: prefix, and only nxds tokens are available.

Step 3: Add the appearance plugin

The plugin applies the theme and saves the user's choices, like dark mode. A script in index.html applies the saved theme before the first paint.

Install the theme engine and the Vue component dependencies:

npm install @nxds/core reka-ui @vueuse/core

Copy packages/vue/src/components/appearance/ into src/components/appearance/.

Set your app's starting theme:

// src/appearance-state.ts
import { DEFAULT_NXDS_APPEARANCE, type NxdsAppearanceState } from '@nxds/core';

export const defaultAppearance = {
  ...DEFAULT_NXDS_APPEARANCE,
  brandColor: '#2563eb',
} satisfies NxdsAppearanceState;

Add Tailwind, the @ import alias and the theme script to vite.config.ts:

// vite.config.ts
import {
  createNxdsAppearanceBootstrapScript,
  createNxdsAppearanceSnapshotFromState,
} from '@nxds/core';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';

import { defaultAppearance } from './src/appearance-state';

const defaultSnapshot =
  createNxdsAppearanceSnapshotFromState(defaultAppearance);

export default defineConfig({
  plugins: [
    vue(),
    tailwindcss(),
    {
      name: 'nxds-appearance-bootstrap',
      transformIndexHtml(html) {
        const script = createNxdsAppearanceBootstrapScript({
          storageKey: 'app-appearance',
          defaultSnapshot,
        });
        return html.replace('</head>', `<script>${script}</script></head>`);
      },
    },
  ],
  resolve: {
    alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
  },
});

Install the plugin with the same storage key:

// src/main.ts
import { createApp } from 'vue';

import App from './App.vue';
import { defaultAppearance } from './appearance-state';
import { createNxdsAppearance } from './components/appearance';

import './style.css';

createApp(App)
  .use(
    createNxdsAppearance({
      storageKey: 'app-appearance',
      defaultState: defaultAppearance,
    })
  )
  .mount('#app');

Give the page the nxds background, text color and font in index.html:

<body
  class="nx:bg-background nx:text-foreground nx:typography-body-default"
></body>

Step 4: Add the cn helper

npm install clsx tailwind-merge class-variance-authority

Copy packages/vue/src/lib/utils.ts to src/lib/utils.ts.

cn merges class names and resolves conflicts between nx: classes. That's what lets <Button class="nx:bg-error-background" /> replace the button's own background.

Add your first component

Each component lists the components to install first, the packages to install, and the files to copy. For Button:

Install these components first: ButtonGroup, Spinner.

components/button/Button.vue
components/button/index.ts
lib/slots.ts

Paths are relative to packages/vue/src/ in the repo. Keep the same folders under src/, and the imports between files work without edits.

Render it:

<script setup lang="ts">
import { Button } from '@/components/button';
</script>

<template>
  <Button>Button</Button>
</template>

To check dark mode, add a toggle that uses useNxdsAppearance:

<!-- src/components/ModeToggle.vue -->
<script setup lang="ts">
import { useNxdsAppearance } from '@/components/appearance';
import { Button } from '@/components/button';

const { state, setState, resolvedMode } = useNxdsAppearance();

function toggle() {
  setState({
    ...state.value,
    mode: resolvedMode.value === 'dark' ? 'light' : 'dark',
  });
}
</script>

<template>
  <Button @click="toggle">Mode: {{ resolvedMode }}</Button>
</template>

Check your setup

  • nxds.css is imported from src/style.css
  • main.ts installs createNxdsAppearance
  • Reloading in dark mode doesn't flash light first
  • The mode you pick is still set after a full reload

If the button has no styles, nxds.css isn't imported (step 2). If the page flashes on reload, check that the script and the plugin use the same storage key.

Where the files are

FilePath in the repo
Token CSSpackages/tailwind/*.css
Appearance pluginpackages/vue/src/components/appearance/
cn helperpackages/vue/src/lib/utils.ts
Componentspackages/vue/src/components/{name}/

Next steps

  • Appearance: every theme field, and how to change it at runtime.
  • Playground: try a theme on every component, then copy it into your app.
  • Color: how the palette is built.
  • Components: every component, with its source to copy.