UI95

Theming

There are different kinds of retro vibes.

CSS variables

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.

Usage
<div className="bg-card text-card-foreground pixel-border border-border" />

Setup

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.

app/layout.tsx
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>
  )
}

Inks

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.

Token convention

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.

app/globals.css
:root {
  --primary: var(--ink-0);
  --primary-foreground: var(--ink-3);
}
Usage
<div className="bg-primary text-primary-foreground">Hello</div>

Theme tokens

These live under :root and .dark. The swatches show the current palette, and the used-by column is read from the component sources.

Pixel scale

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.

app/globals.css
:root {
  --pixel: 2px;
}

<div className="pixel-border border-border offset-shadow-2" />
<div className="border-b-(length:--pixel) border-border" />

Utilities

The theme ships these utilities alongside the tokens.

UtilityWhat it doesUsed by
dither-25Masks a surface to a 25% Bayer pattern.alert-dialogdialogdrawerscroll-areasheet
dither-50Masks a surface to a 50% checkerboard.buttoncalendardrawerresizableslider+2 more
dither-75Masks a surface to a 75% Bayer pattern.Theme only
stripesHorizontal title-bar stripes in the current text color.alert-dialogdialogsheetwindow
segmentsVertical segments, as in the progress bar.progress
pixel-borderA solid border one pixel unit wide.accordionalertalert-dialogaspect-ratioavatar+36 more
bevel-raisedGlint top-left, lowlight bottom-right.accordionbadgebuttoncalendarcard+11 more
bevel-sunkenThe raised bevel inverted, for pressed states.accordionbadgebuttonnavigation-menuresizable+4 more
wellA dark recessed field with a rim and an outer shade.calendarcheckboxcommandinputinput-group+6 more
dither-shadowA dithered drop shadow drawn only outside the element.window
bevel-frameA 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-noneRemoves the offset shadow and keeps any bevel.accordionbadgebuttoncalendarcard+11 more
pixelatedNearest-neighbour image scaling.dither-imageskeleton
animate-blinkA stepped on-off blink, for cursors.input-otpterminal

Adding new tokens

Define the token under :root and .dark, pointing at an ink, then expose it to Tailwind with @theme inline.

app/globals.css
: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.

Palettes

data-theme on <html> picks the palette and the dark class picks its dark variant. Swatches run from --ink-0 to --desktop.

Phosphor
data-theme="phosphor"
Light
Dark
Cyan Pixel
data-theme="cyan-pixel"
Light
Dark
Retro Candy
data-theme="retro-candy"
Light
Dark
Soul Green
data-theme="soul-green"
Light
Dark
Win95
data-theme="95"
Light
Dark

To add a palette, set the six inks for light and dark under its own data-theme. This is how Win95 is defined:

app/globals.css
[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);
}

Theme CSS

This is the full CSS the theme item writes into your project, generated from the registry.

Sound

Sound is opt-in. The sound guide covers how it works and how to add it.