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.
npm install -D sass @nuvoui/coreSet up
Write the configuration once
A partial that forwards
@nuvoui/core/mixinswith 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);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";Import it in the root layout
The root layout wraps every route, so every page gets the tokens.
src/app/layout.tsximport 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.
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.
@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));
}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>.
<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.