Skip to content
NuvoUI

Setup

Next.js

Configure NuvoUI once, load the global stylesheet in the root layout, and style components with CSS modules and apply(). Works with Turbopack and webpack.

Requires @nuvoui/core 2.1.0 or later.

Install

Next.js compiles .scss files once sass is installed. @use "@nuvoui/core" resolves with no change to next.config.ts, under Turbopack (the default) and under next build --webpack.

Terminal
npm install -D sass @nuvoui/core

Set up

  1. Write the configuration once

    A partial that forwards @nuvoui/core/mixins with your settings. The mixins entry has the same API as @nuvoui/core (apply(), every mixin, token(), with()) and outputs no CSS.

    src/styles/_nuvo.scss
    @forward "@nuvoui/core/mixins" with ($brand: #4361ee, $generate-utility-classes: true);
  2. Create the global stylesheet

    It emits the tokens, the reset, the base styles and the utility classes with your configuration. @use "nuvo" comes first, so core loads with the configured brand. Next.js accepts it as a global stylesheet because it is imported from the root layout.

    src/styles/global.scss
    @use "nuvo";
    @use "@nuvoui/core";
  3. Import it in the root layout

    The root layout wraps every route, so every page gets the tokens.

    src/app/layout.tsx
    import type { Metadata } from "next";
    import "../styles/global.scss";
    
    export const metadata: Metadata = { title: "My app" };
    
    export default function RootLayout({ children }: LayoutProps<"/">) {
      return (
        <html lang="en">
          <body>{children}</body>
        </html>
      );
    }

With utility classes

Classes go straight into className, in server and client components alike.

src/app/page.tsx
import Card from "../components/Card";

export default function Home() {
  return (
    <main className="grid gap-4 p-6">
      <div className="flex between x-center gap-3 p-4 bg-surface border rounded-md">
        <span className="font-semibold">Invoices</span>
        <span className="bg-success-subtle text-success text-xs rounded-full px-2 py-1">Paid</span>
      </div>
      <Card title="Quarterly report">Revenue and churn for July to September.</Card>
    </main>
  );
}

With apply()

Each component loads the partial and writes only its own rules. Numbers are configuration keys (p(4) is var(--nu-space-4)), and modifiers come after the utility: state and breakpoint in either order. A CSS module must not load @nuvoui/core itself: its reset selectors are not local, and Turbopack and webpack both stop with a "not pure" selector error.

src/components/Card.module.scss
@use "../styles/nuvo" as *;

.card {
    @include apply(p(4), bg(surface), rounded(md), shadow(md) hover, p(6) md);
}

.title {
    @include apply(m(0), font-size(lg), font(semibold));
}

.text {
    @include apply(mt(2), mb(0), text-color(muted));
}
src/components/Card.tsx
import type { ReactNode } from "react";
import styles from "./Card.module.scss";

export default function Card({ title, children }: { title: string; children: ReactNode }) {
  return (
    <article className={styles.card}>
      <h2 className={styles.title}>{title}</h2>
      <p className={styles.text}>{children}</p>
    </article>
  );
}

Dark mode

With no data-theme on <html>, the tokens follow the operating system setting. Set data-theme="dark" or data-theme="light" to choose one, and change it from a script to switch at runtime: document.documentElement.dataset.theme = "dark". Any element can carry data-theme to theme only its own subtree. Every class and every apply() rule reads the tokens, so nothing else changes. If a script changes the attribute before React hydrates, add suppressHydrationWarning to <html>.

src/app/layout.tsx
<html lang="en" data-theme="dark" suppressHydrationWarning>

What gets emitted

The global stylesheet holds the tokens, the light and dark themes, the reset, the base styles, the tooltips, the @property registrations and, with $generate-utility-classes: true, every utility class: about 1 MB minified, 92 KB gzipped. Without the classes it is about 42 KB. Load it on every page, because the tokens live there.

Card.module.scss compiles to five rules and nothing else: the hashed .card class, its hover, its md padding, the title and the text. The tokens are not repeated per component, and a component cannot reset the configured brand.