Installation
What to install, the two things you have to do yourself, and what you do NOT need to configure.
You do not need the Vanilla Extract plugin
The package compiles its stylesheets in its own build and ships plain CSS next to the JS. Your
bundler only has to resolve an import "./something.css" from node_modules, which every bundler
does. This site is the proof: it runs on Next with Turbopack and installs no plugin.
Wrap your app
import { NebulaProvider, ColorSchemeScript, vars } from "@stellaria/nebula-web";
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ColorSchemeScript defaultTheme="dark" />
</head>
<body
style={{ margin: 0, background: vars.color.surface.base, color: vars.color.text.primary }}
>
<NebulaProvider defaultTheme="dark" applyTheme="root">
{children}
</NebulaProvider>
</body>
</html>
);
}Those are three pieces of one mechanism, and they only work together.
ColorSchemeScript runs before the first paint and writes the theme onto <html>.
applyTheme="root" tells the provider that the theme lives there, so it does not declare a second
one on its own element. And the canvas on body reads the theme variables, because the catalogue
never paints html or body — those are yours.
Leave out the script and applyTheme="root" renders unstyled until hydration: the server has no way
to know which theme the visitor picked last time. If you cannot add the script, drop applyTheme and
the provider will theme its own subtree instead — you get a flash when the stored theme differs from
defaultTheme, which is the trade-off.
Load the typeface
The library does not download fonts, on purpose. Imposing a loading strategy — CDN, next/font,
a bundler plugin — would tie you to one. The theme declares the family; fetching it is yours.
The official themes ask for Geist. The fastest route is the variable Fontsource packages, which cover weights 100–900 in a single file:
import "@fontsource-variable/geist";
import "@fontsource-variable/geist-mono";If you would rather use your own typeface, override font.family in your theme. What you should not
do is skip both: you will silently get your system font, and every size, weight and letter-spacing in
the catalogue was calibrated against Geist.
Do not put the provider under a transformed ancestor
Overlays — Popover, Tooltip, Menu, ContextMenu, Select, Combobox, MultiSelect — portal
into a container the provider owns, so they stay inside the theme. They position themselves with
position: fixed.
An ancestor with transform, filter or contain above the provider creates a containing
block, and fixed starts resolving against that element instead of the viewport. Your overlays will
be offset. The container itself is mounted without overflow constraints so it never causes this on its
own — but a transformed wrapper of your own will.
Then use it
import { Button, Card, Stat } from "@stellaria/nebula-web";
export function Panel() {
return (
<Card withBorder p="lg">
<Stat label="Closing days" value="4" diff="-62 %" trend="down" diffLabel="decrease" />
<Button variant="gradient">Deploy</Button>
</Card>
);
}