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:
- Tailwind 4 — the setup the
nx:prefix depends on. - The token CSS layer — the tokens and utilities, plus the native color-scheme policy.
- The appearance provider — what themes the components and paints before hydration.
- The
cnhelper — 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:
| What | Source in the repo |
|---|---|
| Token CSS (step 2) | packages/tailwind/*.css |
| Appearance provider | packages/react/src/components/appearance/provider/ |
cn helper | packages/react/src/lib/utils.ts |
| Components | packages/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.cssImport 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/coreThe 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 — NexusAppearanceScriptThen 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-mergeCopy 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.0components/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.tsEvery 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-pluginnexusComponentConfig() 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.