Nexus DSdocs

Install

Nexus components are copy/own: you paste a component's source into your app and it is yours from that moment. Nothing installs it, and no upstream change will ever reach it — editing a pasted component means editing your own file, and picking up a later fix means pasting the new version over it.

A pasted component still reaches for four things that are not in the file it came from. Set those up once, in this order, and everything you paste afterwards works:

  1. Tailwind 4 — the setup the nx: prefix depends on.
  2. The token CSS layer — the tokens and utilities, plus the native color-scheme policy.
  3. The appearance provider — what themes the components and paints before hydration.
  4. The cn helper — the module every copied component imports.

The examples target a fresh Next 15 App Router app. Vite wiring lives in Theme setup.

Where the files come from

Everything this page tells you to copy lives in the Nexus repo, at these paths:

WhatSource in the repo
Token CSS (step 2)packages/tailwind/*.css
Appearance providerpackages/react/src/components/appearance/provider/
cn helperpackages/react/src/lib/utils.ts
Componentspackages/react/src/components/{name}/

Take the files by hand, as below. If instead you want a whole standalone design system of your own — the components and the generated token CSS forked into a publishable monorepo under your own scope, still depending on the published @nexus_ds/core for the runtime engine — clone the repo, run pnpm install, then pnpm export --name=your-design-system, and follow that scaffold rather than this page. The export builds @nexus_ds/core on the way through, so the install is not optional.

Step 1: Tailwind 4

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

Do not add an @import "tailwindcss" of your own — including the one a create-next-app scaffold leaves at the top of globals.css. The Nexus token CSS imports Tailwind itself, as @import 'tailwindcss' prefix(nx), and that prefix is where nx:bg-primary-background comes from. prefix() is a property of the build, not of one import, so a second unprefixed import does not buy you a second, unprefixed utility layer: everything stays nx:. It emits preflight twice wherever you put it. Put it after the Nexus import and it also re-injects Tailwind's default @theme on top of Nexus's resets, so

nx:bg-red-500 and a stock nx:text-lg come back alongside the tokens.

Tailwind finds class names by scanning your project, so a pasted component's utilities are generated with no @source line: the file sits in your source tree like any other. @source is only for class names automatic detection cannot reach — node_modules, or files outside your project.

Step 2: The token CSS layer

Copy the token CSS into your app. Seven files, one entry point:

styles/nexus/
├── nexus.css                 # entry — imports Tailwind with prefix(nx), then the six below
├── variables.css             # the --nx-* values, and their .dark overrides
├── typography-utilities.css
├── borderwidth-utilities.css
├── border-color-aliases.css
├── motion-utilities.css
└── spacing-utilities.css

Import the entry point from your app's CSS entry point, followed by the animation utilities some components use for their enter and exit states:

npm install tw-animate-css
/* app/globals.css */
@import '../styles/nexus/nexus.css';
@import 'tw-animate-css';

nexus.css is the whole token surface: the nx: utilities, the semantic color tokens, and the typography, spacing, radius, and motion scales.

This decides how you write Tailwind everywhere in the app, not just inside pasted components: once nexus.css is your Tailwind entry point, nx:flex exists and bare flex does not. Nexus also resets Tailwind's default scales (--color-*, --spacing-*, --text-* and the rest are set to initial), so the utilities you get are the token set, not Tailwind's stock palette. Write your own markup as nx: too.

To add project tokens, put your own @theme block in globals.css after the import — nexus.css is generated output stamped DO NOT EDIT, and pasting a newer copy over it would drop anything you added there.

This assumes a greenfield app. Adding Nexus to one that already has unprefixed Tailwind utilities needs a separate nx entry point rather than a single shared one; that setup is being designed in #604.

Then declare the native color scheme, so the browser themes scrollbars, form controls, and the page canvas before any JavaScript runs:

// app/layout.tsx — step 3 fills the rest of this file in
import type { Viewport } from 'next';

export const viewport: Viewport = { colorScheme: 'light dark' };

Next renders that as <meta name="color-scheme" content="light dark" />; outside Next, write the tag itself. nexus.css emits the matching CSS policy — :root advertises both schemes, :root:not(.dark) pins light, and .dark pins dark — so native UI follows the app's theme instead of the operating system's.

Step 3: The appearance provider

The provider is what turns the token layer into a live theme. It derives the --nx-* values from a single appearance state, toggles .dark, and sets the data-density / data-radius / data-shadow / data-borderwidth attributes the token CSS keys off. Its companion script does that same work inline in <head>, before hydration, so there is no flash of the wrong theme.

The engine behind it is a published package:

npm install @nexus_ds/core

The React wiring is copy/own like the components. Copy the provider in:

components/appearance/provider/
├── index.ts      # client surface — createNexusAppearance, NexusAppearanceProvider, useNexusAppearance
├── provider.tsx
├── factory.tsx
├── script.tsx
└── server.ts     # server-safe — NexusAppearanceScript

Then wire the script and the provider in the root layout — the same app/layout.tsx from step 2, now complete. Its viewport export is the one you already added, not a second one:

// app/layout.tsx — complete
import { DEFAULT_NEXUS_APPEARANCE } from '@nexus_ds/core';
import type { Viewport } from 'next';

import { NexusAppearanceProvider } from '@/components/appearance/provider';
import { NexusAppearanceScript } from '@/components/appearance/provider/server';

import './globals.css';

export const viewport: Viewport = { colorScheme: 'light dark' };

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

Both need the same storageKey: the provider persists the user's choice there, and the script restores it on the next load. Change what the app opens with by overriding fields on the default state — spread it and set mode to 'dark' to open dark, or brandColor and surfaceTone to move the palette.

Theme setup covers the rest: cookie-seeded SSR, Vite's transformIndexHtml, and the storage policies. It picks up from the layout above and shows only what changes, against these same copied modules.

Step 4: The cn helper

Every Nexus component composes its classes through cn, so this file has to exist before the first paste:

npm install clsx tailwind-merge

Copy lib/utils.ts in. It is clsx wrapped in a tailwind-merge instance configured with prefix: 'nx' and the Nexus custom utility groups. That configuration is what lets <Button className="nx:bg-error-background" /> override the variant's own background instead of landing beside it in the class list and losing to source order.

Paste a component

Setup is done. A component is never just its own file: it imports other Nexus files, and those import packages. Each component lists both, generated from its imports. For Button, install the packages, then copy the files — the ones it imports first, its own last:

npm install @radix-ui/react-slot@^1.2.4 @tabler/icons-react@^3.36.1 class-variance-authority@^0.7.1 clsx@^2.1.1 tailwind-merge@^3.4.0
components/button-group/button-group-context.ts
components/spinner/index.ts
components/spinner/spinner.tsx
lib/icons.ts
lib/utils.ts
components/button/button.tsx
components/button/index.ts

Every path is relative to packages/react/src/ in the repo. Keep it as it is under your @/ root, next to the lib/utils.ts from step 4, and the relative imports between the files — barrels included — resolve without an edit. Rerunning an install line or recopying lib/utils.ts changes nothing. A component with its own stylesheet, such as Progress, also lists the @import lines to add to app/globals.css.

Render it:

'use client';

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

export default function ButtonCoreVariants() {
  return (
    <div className="nx:flex nx:flex-wrap nx:gap-3">
      <Button>Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
    </div>
  );
}

Those are this page's own buttons, always styled — the checks below are for the ones in your app. To run the second one you need a way to switch modes, which is the provider's state and setState:

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

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

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

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

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

Render it on the same page as the buttons. It flips off resolvedMode rather than state.mode, so it does the right thing from a 'system' start.

Unstyled buttons mean the token import from step 2 never reached the page. Styled buttons that stay light when you toggle mean the provider from step 3 is not wrapping them.

Lint guardrails

Optional. The conventions in a pasted component are now yours to keep, and the rules that enforce them publish to npm:

npm install -D @nexus_ds/eslint-plugin

nexusComponentConfig() contributes plugins and rules only. Spread it into an entry of your own and append that to your existing config:

// eslint.config.mjs
import { nexusComponentConfig } from '@nexus_ds/eslint-plugin/config';

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

export default eslintConfig;

That turns on nx-class-conventions, no-render-prop-types, and no-multi-statement-jsx-handler over the files you point it at. It relies on a TypeScript parser set earlier in the config, which a create-next-app scaffold provides through next/core-web-vitals. If yours sets none, every .tsx fails to parse and no rule runs: install typescript-eslint too and give the entry languageOptions: { parser: tseslint.parser }.

Next steps

  • Theme setup — pick a brand and surface tone, seed the theme from a cookie, wire Vite.
  • Create — style real Nexus components live, then copy the appearance into your project.
  • Foundations → Color — how the palette is engineered (OKLCH + APCA).
  • Components — the full catalog, each page with the source to copy.