Theming
There are different kinds of retro vibes.
Every component reads semantic tokens such as card, foreground and border. Change the tokens in your CSS and every component follows, with no class edits. Tailwind turns each token into utilities like bg-card, text-foreground and border-border. Dark mode overrides the same tokens inside .dark.
<div className="bg-card text-card-foreground pixel-border border-border" />The default look pairs Silkscreen for titles and labels with VT323 for body text, both free on Google Fonts. Load those, or any pixel display font and monospace body font you prefer, and expose them as --font-display and --font-body (with next/font/google, pass them as the variable option). The data-theme attribute on <html> picks the palette, and the dark class switches it to dark mode.
import { Silkscreen, VT323 } from "next/font/google"
const display = Silkscreen({ subsets: ["latin"], weight: ["400", "700"], variable: "--font-display" })
const body = VT323({ subsets: ["latin"], weight: "400", variable: "--font-body" })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-theme="phosphor" className={`${display.variable} ${body.variable}`}>
<body>{children}</body>
</html>
)
}Each palette has four inks, --ink-0 to --ink-3, plus --ink-4 for glints and fills and --desktop for the ground. Every semantic token points at one of them, so a palette is six values. Shades between inks are dither, never a new color.
Tokens come in surface and foreground pairs. The base token colors the surface and the -foreground token colors the text and icons on it, so primary pairs with primary-foreground.
:root {
--primary: var(--ink-0);
--primary-foreground: var(--ink-3);
}<div className="bg-primary text-primary-foreground">Hello</div>These live under :root and .dark. The swatches show the current palette, and the used-by column is read from the component sources.
UI95 has no radius scale; --radius is 0. Its scale is --pixel, one pixel unit. Borders, offsets and dither cells are whole multiples of it, so offset-shadow-2 drops a shadow two units. Change --pixel and the whole library grows or shrinks on the same grid.
:root {
--pixel: 2px;
}
<div className="pixel-border border-border offset-shadow-2" />
<div className="border-b-(length:--pixel) border-border" />The theme ships these utilities alongside the tokens.
| Utility | What it does | Used by |
|---|---|---|
| dither-25 | Masks a surface to a 25% Bayer pattern. | alert-dialogdialogdrawerscroll-areasheet |
| dither-50 | Masks a surface to a 50% checkerboard. | buttoncalendardrawerresizableslider+2 more |
| dither-75 | Masks a surface to a 75% Bayer pattern. | Theme only |
| stripes | Horizontal title-bar stripes in the current text color. | alert-dialogdialogsheetwindow |
| segments | Vertical segments, as in the progress bar. | progress |
| pixel-border | A solid border one pixel unit wide. | accordionalertalert-dialogaspect-ratioavatar+36 more |
| bevel-raised | Glint top-left, lowlight bottom-right. | accordionbadgebuttoncalendarcard+11 more |
| bevel-sunken | The raised bevel inverted, for pressed states. | accordionbadgebuttonnavigation-menuresizable+4 more |
| well | A dark recessed field with a rim and an outer shade. | calendarcheckboxcommandinputinput-group+6 more |
| dither-shadow | A dithered drop shadow drawn only outside the element. | window |
| bevel-frame | A glint and lowlight frame drawn above the element's children. | window |
| offset-shadow-* | A hard drop shadow N pixel units down and right. | alertalert-dialogbuttoncontext-menudialog+11 more |
| offset-shadow-none | Removes the offset shadow and keeps any bevel. | accordionbadgebuttoncalendarcard+11 more |
| pixelated | Nearest-neighbour image scaling. | dither-imageskeleton |
| animate-blink | A stepped on-off blink, for cursors. | input-otpterminal |
Define the token under :root and .dark, pointing at an ink, then expose it to Tailwind with @theme inline.
:root {
--warning: var(--ink-2);
--warning-foreground: var(--ink-0);
}
.dark {
--warning: var(--ink-1);
--warning-foreground: var(--ink-3);
}
@theme inline {
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}You can now use bg-warning and text-warning-foreground.
data-theme on <html> picks the palette and the dark class picks its dark variant. Swatches run from --ink-0 to --desktop.
To add a palette, set the six inks for light and dark under its own data-theme. This is how Win95 is defined:
[data-theme="95"] {
--ink-0: #000000;
--ink-1: #808080;
--ink-2: #a0a0a0;
--ink-3: #c0c0c0;
--ink-4: #ffffff;
--desktop: #aaaaaa;
--titlebar: #000080;
--titlebar-foreground: var(--ink-4);
--wallpaper-0: #008080;
--wallpaper-1: rgb(1, 117, 117);
--wallpaper-2: #008080;
--wallpaper-3: rgb(1, 136, 136);
--keycap: var(--ink-3);
--keycap-foreground: var(--ink-0);
--keycap-wall-hi: var(--ink-4);
--keycap-wall-lo: var(--ink-1);
--keycap-outline: var(--ink-0);
}
.dark[data-theme="95"] {
--ink-0: #000000;
--ink-1: #404040;
--ink-2: #808080;
--ink-3: #c0c0c0;
--ink-4: #606060;
--desktop: #333333;
--titlebar: #000080;
--titlebar-foreground: var(--ink-3);
--wallpaper-0: #004040;
--wallpaper-1: #045353;
--wallpaper-2: #024a4a;
--wallpaper-3: var(--wallpaper-0);
--keycap: var(--ink-2);
--keycap-foreground: var(--ink-0);
--keycap-wall-hi: var(--ink-3);
--keycap-wall-lo: var(--ink-1);
}This is the full CSS the theme item writes into your project, generated from the registry.
Sound is opt-in. The sound guide covers how it works and how to add it.