Getting started with Next.js
This page walks a brand-new Next.js project (App Router) from create-next-app to a themed, rendering component — first without Tailwind, then with it. The two paths differ only in how the CSS is wired up; the components, the theming, and the TypeScript story are identical.
Every component ships with "use client" already in place, so you import straight from @forte-ui/react — no provider, no registry, no configuration file. Flat exports like Button render fine from server components; the compound components need one "use client" boundary of your own, covered in step 5.
Without Tailwind
1. Create the project
npx create-next-app@latest my-app --typescript --app --no-tailwind
cd my-appAny package manager works — pnpm create next-app and yarn create next-app take the same flags.
2. Install the library
npm install @forte-ui/reactReact 18 or 19 and the matching react-dom are the only required peer dependencies. Base UI comes with the package.
3. Import the stylesheets
Two, imported once at the root. In app/layout.tsx:
import "@forte-ui/react/theme.css";
import "@forte-ui/react/styles/reset.css";
import "./globals.css";theme.css carries the whole system — the tokens, the colour ramps, the motion system, the accessibility responses. Everything in it sits inside @layer forte.*, and a cascade layer loses to unlayered author CSS, so your own stylesheets override the library without !important regardless of load order.
reset.css is the second one, and it is opt-in twice over: importing it does nothing until forte-reset appears on an element. It sets box-sizing: border-box and suppresses the platform tap highlight — the grey box Safari and Chrome paint over whatever a touch lands on, which ignores border-radius and lingers after your finger lifts. The library already suppresses it on its own components; this extends the same treatment to the markup you write. Drop both lines if you would rather keep the platform default — see the tap highlight on touch for the trade-off.
The class is what switches it on. On the same file's <html>:
<html lang="en" className="forte-reset">4. Seed the theme
Replace app/globals.css. The scaffold's --background / --foreground pair hardcodes exactly the two colours the theme should own:
:root {
--forte-accent-seed: #6d43d4;
}
body {
margin: 0;
background: var(--forte-color-background);
color: var(--forte-color-foreground);
font-family: var(--forte-font-sans);
}The seed is the theme: all twelve accent steps, the brand-tinted neutrals, and a readable text colour for solid fills derive from it — in both light and dark mode, with no JavaScript and no build step. The body rule is yours to keep or change; the library styles its components, never your page.
5. Render a component
app/page.tsx — still a server component:
import { Button } from "@forte-ui/react";
export default function Home() {
return (
<main style={{ display: "grid", placeItems: "center", minHeight: "100dvh" }}>
<Button>It works</Button>
</main>
);
}npm run devThat is the whole integration: one install, one CSS import, one variable.
6. Light and dark
Both palettes are already there — with no attribute set, the page follows the OS through prefers-color-scheme. To pin one, set data-theme on the root element:
<html lang="en" className="forte-reset" data-theme="dark">That is what create-forte-ui --scheme dark writes — and it then leaves the toggle and script below out of the layout, since a static attribute has nothing to switch and nothing to replay.
For a switchable theme, the library ships the whole kit:
ThemeToggle is the switch, and ThemeScript in
the layout's <head> replays a stored choice before first paint instead of
flashing. app/layout.tsx — which is where create-forte-ui puts both, along
with the suppressHydrationWarning the script calls for:
import { ThemeScript, ThemeToggle } from "@forte-ui/react";
<html lang="en" className="forte-reset" suppressHydrationWarning>
<head>
<ThemeScript />
</head>
<body>
<ThemeToggle
style={{ position: "fixed", top: "var(--forte-space-4)", right: "var(--forte-space-4)" }}
/>
{children}
</body>
</html>The toggle goes in the layout because it is chrome — one declaration, and every
route the app grows keeps it. It needs no "use client" there for the same
reason Button does not in step 5: it is a flat export, so the layout stays a
server component. On the Tailwind path below the placement is
className="fixed top-4 right-4" instead, reading the same two space tokens.
Every derived colour re-resolves; there is no second stylesheet to load. The same attribute (and .forte-theme / data-forte-theme scoping for islands) is covered in Theming, and useTheme is the hook under the toggle when you want your own control. If you already use next-themes, it drives the same attribute directly.
With Tailwind
The package ships a bridge stylesheet for Tailwind v4 that re-points Tailwind's theme at the forte-ui tokens, so bg-primary is --forte-color-primary and every utility follows the seed, dark mode and theme scopes exactly like the components do.
1. Create the project and install
npx create-next-app@latest my-app --typescript --app --tailwind
cd my-app
npm install @forte-ui/reactcreate-next-app scaffolds Tailwind v4, which is the version the bridge targets.
2. Wire the three stylesheets
Replace app/globals.css entirely:
@import "@forte-ui/react/tailwind.css";
@import "tailwindcss";
@import "@forte-ui/react/theme.css";
@import "@forte-ui/react/styles/reset.css";
:root {
--forte-accent-seed: #6d43d4;
}Everything the scaffold put there goes — its @theme inline block and --background / --foreground pair are precisely the hardcoded colours the seed replaces.
Two orderings in that file are load-bearing:
- The bridge before
tailwindcss. The bridge's first line pins the cascade-layer order totheme, base, forte, components, utilities, and a layer order is fixed at its first appearance. That statement is what keeps Tailwind's Preflight (inbase) from blanking the components, and what keepsutilitiesable to beat them —p-4on aButtonwins because of it. Import Tailwind first and its own layer statement wins the race instead. theme.cssafter the bridge — here, not inlayout.tsx.theme.cssalso declares thefortelayer. Import it inlayout.tsxaboveglobals.cssandfortegets pinned beforebase, at which point Preflight'sbutton { background: transparent }and friends beat every component by layer order alone.
app/layout.tsx keeps importing only ./globals.css.
3. Style with token utilities
Tailwind's stock scales are deleted and rebuilt from the tokens, not extended — bg-slate-800, p-13 and text-white do not compile. That is deliberate: a hardcoded colour would survive review and then ignore the seed, dark mode and every theme scope. Every utility that does compile responds to all of them:
"use client";
import { Button, Card } from "@forte-ui/react";
export default function Home() {
return (
<main className="grid min-h-dvh place-items-center bg-background text-foreground">
<Card.Root variant="elevated" className="items-start gap-5">
<h1 className="text-5 font-semibold">forte-ui + Tailwind</h1>
<p className="text-2 text-foreground-muted">
Utilities and components, one theme.
</p>
<Button>It works</Button>
</Card.Root>
</main>
);
}The "use client" is Card.Root's doing, not Tailwind's — the compound-component callout from the plain path applies here verbatim, and without the directive this page fails to prerender with its exact error.
Note where items-start gap-5 lands: on a component. That is the layer order from step 2 doing its job — utilities beat the card's own layout without !important.
The Light and dark step applies verbatim on this path — the same ThemeScript and ThemeToggle in app/layout.tsx, with the toggle's placement written as className="fixed top-4 right-4".
The full utility mapping, targeting component state from arbitrary variants (data-[loading]:opacity-70), and how to opt a stock scale back in are on the Tailwind page.
4. Merge class lists with cn
Optional, for when you compose class lists conditionally. The package ships a pre-configured merger; it needs tailwind-merge installed (an optional peer dependency — apps that skip this subpath never pay for it):
npm install tailwind-mergeimport { cn } from "@forte-ui/react/cn";
cn("p-4", isWide && "p-6"); // -> "p-6" when isWide holdsA stock twMerge does not know the bridge's renamed scales — it parses text-2 as a text colour and silently drops sizes. The shipped cn (and createCn, for apps that add their own theme keys) is covered on the Tailwind page.
Dark mode with next-themes
forte-ui needs no theming library — ThemeToggle, ThemeScript and useTheme cover the toggle, the persistence and the no-flash script. But if you already reach for next-themes, the two compose directly, because next-themes' whole job is writing an attribute on <html> and data-theme is the attribute forte-ui reads. This works identically on both paths above. One rule: pick one writer — with next-themes installed, skip ThemeScript and forte-ui's own useTheme.
npm install next-themesThe provider is a client component, so give it its own file — app/providers.tsx:
"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="data-theme" disableTransitionOnChange>
{children}
</ThemeProvider>
);
}attribute="data-theme" is the one setting that matters: next-themes' default is a class="dark" on the root element, which forte-ui does not read. The defaults you are not overriding already match — defaultTheme="system" mirrors what forte-ui does with no attribute at all, so adding the provider changes nothing until someone actually toggles. disableTransitionOnChange is optional but worth keeping: without it, every colour transition in the page fires at once on switch.
Wrap the app in app/layout.tsx, and put suppressHydrationWarning on <html> — next-themes sets the attribute from its pre-hydration script, so the server HTML legitimately differs on that one element:
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>A toggle is then forte-ui's own button in controlled mode, driven by next-themes' hook, in any client component:
"use client";
import { useTheme } from "next-themes";
import { ThemeToggle } from "@forte-ui/react";
export function ModeSwitch() {
const { resolvedTheme, setTheme } = useTheme();
return <ThemeToggle theme={resolvedTheme} onThemeChange={setTheme} />;
}On the Tailwind path, nothing more is needed for colours: every token utility resolves through light-dark(), so bg-panel follows the toggle by itself and you will rarely write a dark: variant at all. If you do want one — for a non-colour tweak that differs per theme — note that Tailwind v4's stock dark variant keys on prefers-color-scheme, which ignores the toggle. Re-key it on the attribute, after the bridge import:
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));Loading a font
The library reads one token for its font: --forte-font-sans. Your body rule from step 4 reads it too. So the whole job is: get your font's name into that token. With next/font that is two steps.
Step 1 — load the font, get a variable. In app/layout.tsx:
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
// on the root element:
<html lang="en" className={inter.variable}>This changes nothing visible yet. The class does exactly one thing: it defines --font-inter on <html>. (next/font also self-hosts the font files for you — that part is automatic.)
Step 2 — feed it into the token. In app/globals.css:
:root {
--forte-font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
}Done. Your body rule reads --forte-font-sans, so the page text switches; the components read the same token, so tooltips, menus and dialogs switch with it. One token, everything in lockstep.
Two details worth knowing, neither of which you have to act on:
- Why not just
body { font-family: "Inter" }? It would almost work — but some component parts declarefont-family: var(--forte-font-sans)themselves (floating surfaces cannot rely on inheriting from your page), and those would stay on the system font. Routing through the token is what keeps page and components identical. - Keep the
ui-sans-serif, system-ui, sans-seriftail. It is the fallback if the font fails to load or the variable class is missing — the page degrades to the system font instead of the browser default serif.
On the Tailwind path, font-sans on <body> does the same job as the body rule — the bridge maps it to this exact token.
Where next
- Theming — the seed envelope, the secondary seed, neutral tint, and scoped themes.
- Presets —
data-forte-radius,data-forte-density,data-forte-motion. - Styling components — parts, states, and per-component knobs.
- Theme Studio — pick a seed visually and copy the CSS out.