Full Setup For @neuctra/ui

Follow this comprehensive from-scratch setup guide to configure Neuctra UI with Vite, Next.js, Remix, and Tailwind CSS v4 step by step.

Step 0 — Create React Project

Start with a fresh React project using Vite, Next.js, or Remix.

Vite React

bash
1npm create vite@latest my-neuctra-app -- --template react
bash
1cd my-neuctra-app
2npm install

This creates a new React project with Vite and installs initial dependencies.

Next.js React

bash
1npx create-next-app@latest my-neuctra-app
bash
1cd my-neuctra-app
2npm install

This creates a new React project with Next.js and installs initial dependencies.

Remix React

bash
1npx create-remix@latest my-neuctra-app
bash
1cd my-neuctra-app
2npm install

This creates a new React project with Remix and installs initial dependencies.

Step 1 — Install Dependencies

Install Neuctra UI and Tailwind CSS v4 with required packages.

bash
1npm install @neuctra/ui

Vite or Remix

bash
1npm install -D tailwindcss @tailwindcss/vite

This installs the UI library and Tailwind CSS v4 with the Vite plugin. Note: No PostCSS or Autoprefixer needed with Tailwind v4.

Next.js

bash
1npm install -D tailwindcss @tailwindcss/postcss

This installs the UI library and Tailwind CSS v4 with the PostCSS plugin used by Next.js.

Step 2 — Configure Tailwind v4

Set up Tailwind CSS v4 for your React framework.

Vite

js
1import { defineConfig } from "vite";
2import react from "@vitejs/plugin-react";
3import tailwindcss from "@tailwindcss/vite";
4
5export default defineConfig({
6  plugins: [
7    react(),
8    tailwindcss(),
9  ],
10});

Replace your vite.config.js content with this. Tailwind v4 uses the Vite plugin instead of separate config files.

Next.js

js
1const config = {
2  plugins: {
3    "@tailwindcss/postcss": {},
4  },
5};
6
7export default config;

Add this to postcss.config.mjs in your Next.js project. Tailwind v4 uses the PostCSS plugin for Next.js.

Remix

ts
1import { vitePlugin as remix } from "@remix-run/dev";
2import { defineConfig } from "vite";
3import tsconfigPaths from "vite-tsconfig-paths";
4import tailwindcss from "@tailwindcss/vite";
5
6export default defineConfig({
7  plugins: [
8    remix(),
9    tailwindcss(),
10    tsconfigPaths(),
11  ],
12});

Add the Tailwind v4 Vite plugin to your Remix vite.config.ts or vite.config.js file.

Step 3 — Import Global CSS

Load Tailwind CSS and Neuctra UI sources in the main stylesheet for your React framework.

Vite

Use src/index.css or the CSS file imported in main.jsx / main.tsx.

Next.js

Use app/globals.css and import it from your root app/layout.jsx / app/layout.tsx.

Remix

Use app/tailwind.css, app/root.css, or the global CSS file imported from app/root.jsx / app/root.tsx.

css
1@import "tailwindcss";
2@source "../node_modules/@neuctra/ui";

Step 4 — Add Theme System (Tailwind v4)

Define CSS variables and theme tokens using Tailwind v4's @theme syntax.

css
1@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
2@import "tailwindcss";
3@source "../node_modules/@neuctra/ui";
4@source "./src/**/*.{js,ts,jsx,tsx}";
5@source "./app/**/*.{js,ts,jsx,tsx}";
6
7/* =============================
8   BASE GLOBAL STYLES
9============================= */
10
11body {
12  font-family: "Poppins", sans-serif;
13}
14
15button {
16  cursor: pointer;
17}
18
19/* ===== Custom Scrollbar ===== */
20::-webkit-scrollbar {
21  width: 3px;
22  height: 3px;
23}
24
25::-webkit-scrollbar-track {
26  background-color: #000;
27}
28
29::-webkit-scrollbar-thumb {
30  background-color: var(--primary);
31  border-radius: 9999px;
32}
33
34::-webkit-scrollbar-thumb:hover {
35  background-color: var(--primary);
36}
37
38/* Add light or dark theme variables here */
39/* you can select only one theme or both, depending on your needs */
40/* If you want dark theme remove root and add .dark class to your body or html element */
41
42:root {
43  --primary: #00c214;
44  --primary-foreground: #ffffff;
45
46  --background: #ffffff;
47  --foreground: #09090b;
48  --secondary: #3f3f46;
49
50  --muted: #f4f4f5;
51  --muted-foreground: #71717a;
52
53  --accent: #e4e4e7;
54  --accent-foreground: #09090b;
55
56  --border: #e4e4e7;
57  --input: #f4f4f5;
58  --ring: #a1a1aa;
59
60  --destructive: #d40000;
61  --destructive-foreground: #fafafa;
62
63  --card: #ffffff;
64  --card-foreground: #09090b;
65  --popover: #ffffff;
66  --popover-foreground: #09090b;
67
68  --success: #16a34a;
69  --success-foreground: #fafafa;
70  --warning: #d97706;
71  --warning-foreground: #fafafa;
72  --info: #2563eb;
73  --info-foreground: #fafafa;
74}
75
76.dark {
77  --primary: #00c214;
78  --primary-foreground: #ffffff;
79
80  --background: #09090b;
81  --foreground: #fafafa;
82  --secondary: #d4d4d8;
83
84  --muted: #27272a;
85  --muted-foreground: #a1a1aa;
86
87  --accent: #18181b;
88  --accent-foreground: #fafafa;
89
90  --border: #27272a;
91  --input: #18181b;
92  --ring: #3f3f46;
93
94  --destructive: #e7000b;
95  --destructive-foreground: #fafafa;
96
97  --card: #09090b;
98  --card-foreground: #fafafa;
99  --popover: #18181b;
100  --popover-foreground: #fafafa;
101
102  --success: #22c55e;
103  --success-foreground: #052e16;
104  --warning: #f59e0b;
105  --warning-foreground: #451a03;
106  --info: #3b82f6;
107  --info-foreground: #eff6ff;
108}
109
110/* =============================
111   TAILWIND TOKENS
112============================= */
113@theme {
114  --color-primary: var(--primary);
115  --color-primary-foreground: var(--primary-foreground);
116
117  --color-background: var(--background);
118  --color-foreground: var(--foreground);
119  --color-secondary: var(--secondary);
120
121  --color-muted: var(--muted);
122  --color-muted-foreground: var(--muted-foreground);
123
124  --color-accent: var(--accent);
125  --color-accent-foreground: var(--accent-foreground);
126
127  --color-border: var(--border);
128  --color-input: var(--input);
129
130  --color-ring: var(--ring);
131
132  --color-destructive: var(--destructive);
133  --color-destructive-foreground: var(--destructive-foreground);
134
135  --color-card: var(--card);
136  --color-card-foreground: var(--card-foreground);
137  --color-popover: var(--popover);
138  --color-popover-foreground: var(--popover-foreground);
139  --color-success: var(--success);
140  --color-success-foreground: var(--success-foreground);
141  --color-warning: var(--warning);
142  --color-warning-foreground: var(--warning-foreground);
143  --color-info: var(--info);
144  --color-info-foreground: var(--info-foreground);
145}

The @theme block maps CSS variables to Tailwind's color system. This replaces the old theme.extend approach.

Step 5 — Create Theme Context

Create a global theme context to manage light and dark mode across your entire application.

1"use client";
2
3import React, { createContext, useContext, useEffect, useState } from "react";
4
5const ThemeContext = createContext(undefined);
6
7const getInitialTheme = () => {
8  if (typeof window === "undefined") return false;
9
10  const saved = window.localStorage.getItem("theme");
11  const dark = saved
12    ? saved === "dark"
13    : window.matchMedia("(prefers-color-scheme: dark)").matches;
14
15  document.documentElement.classList.toggle("dark", dark);
16  return dark;
17};
18
19export const ThemeProvider = ({ children }) => {
20  const [isDark, setIsDark] = useState(getInitialTheme);
21
22  useEffect(() => {
23    document.documentElement.classList.toggle("dark", isDark);
24  }, [isDark]);
25
26  // React to OS theme changes while the app is open, unless the user
27  // has already made an explicit choice.
28  useEffect(() => {
29    const media = window.matchMedia("(prefers-color-scheme: dark)");
30
31    const handleChange = (event) => {
32      const saved = window.localStorage.getItem("theme");
33      if (saved) return;
34      setIsDark(event.matches);
35    };
36
37    media.addEventListener("change", handleChange);
38    return () => media.removeEventListener("change", handleChange);
39  }, []);
40
41  const toggleTheme = () => {
42    setIsDark((prev) => {
43      const next = !prev;
44      window.localStorage.setItem("theme", next ? "dark" : "light");
45      return next;
46    });
47  };
48
49  const setTheme = (theme) => {
50    window.localStorage.setItem("theme", theme);
51    setIsDark(theme === "dark");
52  };
53
54  return (
55    <ThemeContext.Provider value={{ isDark, toggleTheme, setTheme }}>
56      {children}
57    </ThemeContext.Provider>
58  );
59};
60
61export const useTheme = () => {
62  const context = useContext(ThemeContext);
63  if (!context) {
64    throw new Error("useTheme must be used within ThemeProvider");
65  }
66  return context;
67};

Create src/contexts/ThemeContext.jsx (or .tsx for TypeScript projects) and add this code. It reads the saved preference (or system preference) and applies the dark class before paint — via the lazy useState initializer, not a post-mount effect — so there's no flash of the wrong theme on load, and it stays in sync with OS theme changes via a matchMedia listener as long as you haven't made an explicit choice.

Running npx @neuctra/ui-cli@latest init generates this exact file for you automatically — .tsx if it detects a tsconfig.json or a TypeScript entry file, .jsx otherwise.

Step 6 — Wrap Your Application

Wrap your application with ThemeProvider so theme state can be accessed anywhere.

The snippets below use plain JSX with no type annotations, so they work identically whether your entry file is .jsx or .tsx.

Vite

jsx
1import React from "react";
2import ReactDOM from "react-dom/client";
3import App from "./App";
4
5import { ThemeProvider } from "./contexts/ThemeContext";
6
7ReactDOM.createRoot(
8  document.getElementById("root")
9).render(
10  <React.StrictMode>
11    <ThemeProvider>
12      <App />
13    </ThemeProvider>
14  </React.StrictMode>
15);

Wrap your root application with ThemeProvider insidemain.jsx or main.tsx. This makes theme controls available throughout your application.

Next.js App Router

jsx
1"use client";
2
3import { ThemeProvider } from "../contexts/ThemeContext";
4
5export function Providers({ children }) {
6  return (
7    <ThemeProvider>
8      {children}
9    </ThemeProvider>
10  );
11}
jsx
1import "./globals.css";
2import { Providers } from "./providers";
3
4export default function RootLayout({ children }) {
5  return (
6    <html lang="en">
7      <body>
8        <Providers>
9          {children}
10        </Providers>
11      </body>
12    </html>
13  );
14}

Wrap your Next.js root layout with ThemeProvider through a client Providers component.

Remix

jsx
1import {
2  Links,
3  Meta,
4  Outlet,
5  Scripts,
6  ScrollRestoration,
7} from "@remix-run/react";
8
9import { ThemeProvider } from "./contexts/ThemeContext";
10
11export default function App() {
12  return (
13    <html lang="en">
14      <head>
15        <Meta />
16        <Links />
17      </head>
18      <body>
19        <ThemeProvider>
20          <Outlet />
21        </ThemeProvider>
22        <ScrollRestoration />
23        <Scripts />
24      </body>
25    </html>
26  );
27}

Wrap your Remix root route with ThemeProvider so all nested routes can access theme controls.

Step 7 — Add Theme Toggle

Use the built-in Neuctra UI ThemeToggle component to switch between light and dark modes.

jsx
1import { ThemeToggleButton } from "@neuctra/ui";
2import { useTheme } from "./contexts/ThemeContext";
3
4export default function App() {
5  const theme = useTheme();
6
7  return (
8    <div className="p-10">
9      <ThemeToggleButton context={theme} />
10    </div>
11  );
12}

Neuctra UI includes a ready-made theme toggle component. Simply pass the theme context returned from useTheme() to theThemeToggleButton component.

Step 8 — Verify Theme Setup

Test that your theme system and Neuctra UI components are working correctly.

jsx
1import { Button, Input } from "@neuctra/ui";
2import { ThemeToggleButton } from "@neuctra/ui";
3import { useTheme } from "./contexts/ThemeContext";
4
5export default function App() {
6  const theme = useTheme();
7
8  return (
9    <div className="min-h-screen bg-background text-foreground p-8">
10      <div className="max-w-md mx-auto space-y-4">
11        <ThemeToggleButton context={theme} />
12
13        <Input placeholder="Test input" />
14
15        <Button className="w-full">
16          Test Button
17        </Button>
18      </div>
19    </div>
20  );
21}

If the background, text colors, and ThemeToggleButton switch correctly between light and dark mode, your theme setup is complete.

Troubleshooting

Tailwind classes not working

Ensure you've imported Tailwind CSS correctly in your main CSS file:

css
1@import "tailwindcss";
2@source "../node_modules/@neuctra/ui";
3@source "./src/**/*.{js,ts,jsx,tsx}";

Build errors with Vite

Make sure your vite config includes the Tailwind plugin:

js
1import tailwindcss from "@tailwindcss/vite";
2
3export default defineConfig({
4  plugins: [react(), tailwindcss()],
5});

Theme colors not applying

Verify your @theme block is properly configured. All CSS variables support full passthrough via ...rest in your component system.

css
1@theme {
2  --color-primary: var(--primary);
3  --color-background: var(--background);
4  --color-foreground: var(--foreground);
5  --color-secondary: var(--secondary);
6}

Dark mode not switching

Check that your CSS has both :root and .dark variables. These values are fully extensible via ...rest props in your theme system.

css
1:root {
2  --background: #ffffff;
3  --foreground: #0f172a;
4}
5
6.dark {
7  --background: #0a0a0a;
8  --foreground: #ffffff;
9}

UI not styled / Tailwind not installed

If you're not using Tailwind CSS, import the Neuctra UI stylesheet directly in your main entry file:

js
1import "@neuctra/ui/dist/ui.css";

Frequently Asked Questions

Which React frameworks does Neuctra UI support?

Neuctra UI works with Vite, Next.js, and Remix. Tailwind CSS v4 is configured slightly differently per framework (a Vite plugin for Vite/Remix, a PostCSS plugin for Next.js), but the component library and theme system are identical across all three.

Do I need to manually write a theme context?

No — running `npx @neuctra/ui-cli@latest init` generates the theme context file for you automatically (.tsx if it detects TypeScript, .jsx otherwise) and wraps your app with ThemeProvider. Manual setup is only needed if you want full control over the file.

How do I avoid a flash of the wrong theme on page load?

Apply the .dark class before paint rather than in a post-mount effect — the theme context's lazy useState initializer reads localStorage/prefers-color-scheme and sets the class synchronously on first render, so there's no flash.