Vite
Set up Nizivo in a Vite 8 single-page app.
Everything on this page is checked by the Vite fixture (fixtures/vite, Vite 8.3.4 with
@vitejs/plugin-react 6.1.2, vite build and vite preview) on the packed packages, except where a
note says otherwise.
Install
pnpm add @nizivo/react@next @nizivo/tokens@next@nizivo/react needs react and react-dom ^19.2.0.
Import the CSS once
Nizivo's JavaScript never imports CSS. In main.tsx, before rendering, import the tokens, the
accessibility floor (base.css), then one stylesheet per component you use:
import '@nizivo/tokens/tokens.css';
import '@nizivo/react/css/base.css';
import '@nizivo/react/css/button.css';
import '@nizivo/react/css/select.css';Or import one file, @nizivo/react/styles.css, which holds the tokens, base.css and every
component's stylesheet. Import either it or the files above, never both (it already holds a
byte-identical copy of tokens.css). The fixture checks the granular files.
Set the CSS target
Nizivo's colours are light-dark() values. Vite 8 minifies CSS for its default target, which is older
than light-dark(), so a plain build rewrites every one of them into a fallback that cannot follow a
nested data-nz-theme scope or an inverse surface (a tone="inverse" Card, a Tooltip). Set the CSS
target to Nizivo's support floor:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react()],
build: {
cssTarget: ['chrome123', 'edge123', 'firefox129', 'safari17.5', 'ios17.5'],
},
});Measured by the fixture: without cssTarget, Vite 8.3.4 kept 0 of the 68 light-dark() values in
tokens.css; with it, all 68. A browserslist in package.json does not change this, because
Vite reads only build.cssTarget.
Theme
Set the theme on <html> in index.html. data-nz-theme="system" follows the operating system with
no script and no flash; light or dark forces one theme. A switch that remembers the user's choice
must apply it before first paint. Nizivo's themeScript does that from <head>: it reads the saved
choice from localStorage (key nz-theme, value light, dark or system), sets data-nz-theme on
<html>, and sets data-nz-platform so Kbd shows ⌘ or Ctrl from the first frame. Let a small plugin
inject it from the installed package, so the copy never drifts from the version you run:
import { themeScript } from '@nizivo/react/theme-script';
function nizivoThemeScript() {
return {
name: 'nizivo-theme-script',
transformIndexHtml: () => [{ tag: 'script', children: themeScript, injectTo: 'head' }],
};
}
// plugins: [react(), nizivoThemeScript()]<!doctype html>
<html lang="en" data-nz-theme="system">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>With nothing stored, or storage blocked, the attribute stays as index.html set it. Your theme switch
saves the choice with localStorage.setItem('nz-theme', theme) and sets the attribute itself; see
Next.js for the details and the CSP hash.
The fixture checks this setup on every frame from first paint: the OS preference, a stored choice and a static attribute are right from the first frame, and a theme set late is caught as a flash.
The dev server
vite (dev) injects the CSS from JavaScript, so the first few frames of a page load paint before any
stylesheet exists. With a dark theme (the OS, a stored choice or the attribute), the fixture measured
about 3 frames (under 40 ms) of the browser's default light canvas before the styles apply; after
that every frame is right. A production build (vite build) links the CSS in <head> and has none.
It is a dev-only effect of how Vite serves CSS, not of Nizivo. If it bothers you, a few inline rules
in index.html paint the right canvas before the styles arrive (not checked by the fixture):
<style>
:root[data-nz-theme='dark'] {
color-scheme: dark;
}
:root[data-nz-theme='system'] {
color-scheme: light dark;
}
</style>Imports and bundle size
Import from subpaths (@nizivo/react/button) or from the barrel (@nizivo/react): Vite drops every
module a page does not use, so both give the same bundle. In the fixture, a page with three Buttons
through the barrel was the same size as the same page through the subpath, within a few bytes, and a page that imports
Select through the barrel shipped only Select's modules. @nizivo/icons tree-shakes the same way: three
imported icons ship three icon modules and the shared base.
Vite 8.3.4 prints no warning for the 'use client' directives in @nizivo/react; a single-page app
treats them as plain strings.
Server rendering
Vite's own SSR (vite build --ssr) needs no ssr.noExternal entry: Nizivo's JavaScript never imports
CSS, so the server build keeps @nizivo/* external and Node loads every module as published. Link
the stylesheets from the client build, as above, and hydrate with hydrateRoot. The Vite SSR fixture
(fixtures/vite-ssr: renderToString behind a plain node:http server) checks this: both builds
print no warning, every page hydrates with no console message, and Select, Dialog, Tooltip, Menu,
Tabs and the form controls work after hydration. For React Router, see
React Router.
Not covered by the fixtures yet
The fixtures run in Chromium; they check the dev server's theme frames (above), and run axe in light,
dark, forced colours and with reduced motion. Streaming SSR (renderToPipeableStream with Suspense)
is checked in the React Router and Next.js fixtures, not in the Vite SSR one.