Everything the framework does
Install it, theme it, and use the components and JavaScript API. Every claim on this page is enforced by a check that runs in CI, so the docs and the build cannot drift apart.
npm install @phantompixeldev/retrocss
- Bundlers & TypeScript ESM, CJS, the exports map and shipped types
- Theming & dark mode One token set; follows the OS with no flash
- Right-to-left Layout mirrors; the Win9x bevels do not
- Accessibility Keyboard operation and WCAG AA, both themes
- Migration guide What changed in 4.0, and why
Getting Started
RetroCSS is a retro-inspired CSS framework for building classic Win9x style UIs with modern CSS and JS. It provides SASS variables, utility classes, components, and a JS API for interactive features.
<link rel="stylesheet" href="proxy.php?url=https%3A%2F%2Fphantompixeldev.github.io%2FRetroCSS%2Fdist%2Fretro.min.css">
<script src="proxy.php?url=https%3A%2F%2Fphantompixeldev.github.io%2FRetroCSS%2Fdist%2Fretro.min.js"></script>
Installation
NPM
npm install @phantompixeldev/retrocss
CDN
<link rel="stylesheet" href="proxy.php?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fnpm%2F%40phantompixeldev%2Fretrocss%2Fdist%2Fretro.min.css">
<script src="proxy.php?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fnpm%2F%40phantompixeldev%2Fretrocss%2Fdist%2Fretro.min.js"></script>
With a bundler
The package ships ESM, CJS and a browser IIFE behind an
exports map, so a bare import resolves to JavaScript and
the stylesheet has its own entry point.
// JavaScript
import RetroCSS from '@phantompixeldev/retrocss'; // ESM
const RetroCSS = require('@phantompixeldev/retrocss'); // CJS
// Styles — pick one
import '@phantompixeldev/retrocss/css'; // full
import '@phantompixeldev/retrocss/css/min'; // minified
Importing the bundle is safe during server-side rendering: it touches no browser global until the DOM exists, so Next, Remix and Astro can import it at the top level.
Sass
Import the source to compile against your own token values.
@use '@phantompixeldev/retrocss/scss' as retro;
TypeScript
Type definitions ship in the package and are picked up automatically
— no @types install.
import RetroCSS, { RetroTheme, RetroHue } from '@phantompixeldev/retrocss';
const theme: RetroTheme = 'dark'; // 'light' | 'dark'
const hue: RetroHue = 'success'; // every hue with a full token set
RetroCSS.applyTheme(theme);
RetroCSS.toast.show('Saved', { type: hue, duration: 0 });
Manual
<link rel="stylesheet" href="proxy.php?url=https%3A%2F%2Fphantompixeldev.github.io%2FRetroCSS%2Fdist%2Fretro.min.css">
<script src="proxy.php?url=https%3A%2F%2Fphantompixeldev.github.io%2FRetroCSS%2Fdist%2Fretro.min.js"></script>
Usage
RetroCSS provides a wide range of components and utilities to build retro-styled interfaces. Here's a simple card example:
<div class="retro-card">
<div class="retro-card-header">System Properties</div>
<div class="retro-card-content">
<p>Welcome to RetroCSS!</p>
<button class="retro-btn retro-btn-primary">OK</button>
</div>
</div>
See the Components section for more examples.
Components
- Buttons:
.retro-btn,.retro-btn-primary, ... - Cards:
.retro-card,.retro-card-header, ... - Alerts:
.retro-alert,.retro-alert-success, ... - Tables:
.retro-table,.retro-table-bordered,.retro-table-striped,.retro-table-sortable, ... - Badges:
.retro-badge,.retro-badge-primary, ... - Navs:
.retro-nav,.retro-nav-item, ... - Modals:
.retro-modal,.retro-modal-content, ... - Accordion:
.retro-accordion,.retro-accordion-toggle, ... - Sidebar:
.retro-sidebar,.retro-sidebar-header - Lists:
.retro-list,.retro-list-bordered, ... - ...and many more! See the demo for all available classes.
JavaScript API
RetroCSS includes a modular JavaScript API for interactive components. Each component is available through the global RetroCSS object.
Initialization
// Auto-initialization happens on DOMContentLoaded
// If you need to manually initialize:
RetroCSS.init();
Toast
RetroCSS.toast.show('Message', { type: 'success', duration: 3000, html: false });
Modal
RetroCSS.modal.show('modalId');
RetroCSS.modal.hide('modalId');
Form Validation
RetroCSS.form.validate(formElement);
RetroCSS.form.validateField(inputElement);
Table sorting
Add .retro-table-sortable. Headings become focusable and
sort on click, Enter or Space, and carry
aria-sort so the direction is announced.
<table class="retro-table retro-table-sortable">
<thead>
<tr>
<th>Name</th> <!-- sniffed: number or text -->
<th data-sort="number">Size</th>
<th data-sort="none">Actions</th> <!-- opts the column out -->
</tr>
</thead>
<tbody>
<tr>
<td>README</td>
<td data-sort-value="2048">2 KB</td>
<td><button class="retro-btn retro-btn-sm">Edit</button></td>
</tr>
</tbody>
</table>
RetroCSS.table.init(); // only needed for tables added after load
data-sort-value sorts a formatted cell by what it means
rather than how it reads. The numeric sniff is strict on purpose: a
loose parseFloat reads 2023-09-01 as
2023 and SKU-001 as -1, which
silently sorts a date column by year and an ID column by the digits
after the first dash.
Code blocks
Any <pre class="retro-code"><code> gets a copy
button automatically. Outside a secure context, where
navigator.clipboard is unavailable, it selects the code
and prompts for Ctrl+C instead.
Dropdown
RetroCSS.dropdown.init();
File Upload
RetroCSS.fileUpload.init();
Events System
// Subscribe to an event
RetroCSS.events.on('init', data => {
console.log('Framework initialized at:', data.timestamp);
});
// Emit a custom event
RetroCSS.events.emit('custom:event', { foo: 'bar' });
// Remove an event listener
RetroCSS.events.off('init', myHandler);
Using a script tag
<script src="proxy.php?url=https%3A%2F%2Fphantompixeldev.github.io%2FRetroCSS%2Fdist%2Fretro.min.js"></script>
<script>
RetroCSS.toast.show('Saved', { type: 'success' });
RetroModal.show('myModal');
</script>
The bundle exposes RetroCSS plus the component singletons
(RetroModal, RetroToast, RetroForm,
RetroTable, RetroDropdown, RetroFileUpload,
RetroEvents) on window.
Theming & Dark Mode
Colours are organised in four tiers. Picking the right tier is the whole trick — a value tuned as a background is usually unreadable as text.
| Token | Role |
|---|---|
--retro-primary | Fill. Background of a filled badge, button or alert. |
--retro-primary-fg | On-fill. Text placed on that fill. |
--retro-primary-text | On-surface. That hue used as text on a page background. |
--retro-primary-hover / -active | States. Fill under :hover / :active. |
All four exist for every hue: primary, success, danger, warning, info, teal, tan,
pink, lime, cyan, orange, brown, violet, gray, maroon, gold, navy, olive, silver.
Every pair clears WCAG AA (4.5:1) in both themes — run npm run check:a11y to verify.
Surface and text tokens: --retro-body-bg (page), --retro-bg (raised surface),
--retro-text, --retro-text-muted, --retro-text-inverse.
--retro-black and --retro-white still work and still invert between
themes, but --retro-text / --retro-text-inverse say what they mean.
Override SASS variables before importing retro.scss, or override the custom
properties at runtime:
:root {
--retro-primary: #0000aa; /* fill */
--retro-primary-fg: #ffffff; /* text on that fill */
--retro-primary-text: #000080; /* that hue as text on a page background */
--retro-font: 'MS Sans Serif', Arial, sans-serif;
}
[data-theme="dark"] {
--retro-body-bg: #181818;
--retro-bg: #232323;
--retro-primary: #4a90e2;
--retro-primary-fg: #000000; /* dark fills are light, so they want black on top */
--retro-primary-text: #4b91e2;
}
Typography is tokenised too: --retro-font, --retro-font-heading,
--retro-font-mono, a --retro-font-size-xs … -3xl
scale in rem, --retro-line-height, and --retro-font-weight-*.
Headings default to the body stack; for a modern heading font set
--retro-font-heading: 'Segoe UI', Tahoma, sans-serif;.
Use the theme toggle button in the navbar to switch between light and dark mode.
Utilities
Atomic helpers for layout and colour, all driven by the same tokens as the
components. Everything below is prefixed retro-.
Layout
<div class="retro-main-layout"> <!-- sidebar + content shell -->
<aside class="retro-sidebar">…</aside>
<div class="main-content">…</div>
</div>
- Flex:
retro-flex,retro-flex-col,retro-flex-wrap,retro-flex-1,retro-items-center,retro-justify-between,retro-self-center - Spacing:
retro-m{t,b,l,r,x,y}-0…8andretro-p…, plusretro-gap-0…8andretro-mx-auto. Scale: 0, 4, 8, 12, 16, 24, 32, 40, 48px. - Sizing:
retro-w-full,retro-max-w-full,retro-max-w-prose,retro-max-w-{sm…xxl} - Effects:
retro-raised,retro-sunken,retro-shadow-{sm,md,lg},retro-rounded
Colour
retro-bg-<hue> sets the fill and its matching on-fill text colour,
so a filled panel is legible in both themes from one class — that pairing is what
npm run check:a11y verifies. retro-bg-<hue>-subtle is a low-alpha
tint of the same hue that keeps normal body text on top.
<div class="retro-bg-primary retro-p-4">Readable in both themes</div>
<div class="retro-bg-success-subtle retro-p-4">Tinted callout</div>
<span class="retro-text-danger">On-surface text</span>
Pick the tier by role: retro-bg-* for a fill,
retro-text-* for text on a page surface. Putting an on-surface colour
inside a filled panel is the one mistake to avoid — it paints the hue on itself.
Prose
retro-prose caps running text at a comfortable measure while leaving
code blocks and tables full width.
Accessibility
Every text/surface pair clears WCAG AA (4.5:1) in both themes.
npm run check:a11y compiles the SCSS and asserts it, and also fails on any
var(--retro-*) that resolves to nothing.
- Focus rings use
:focus-visible— keyboard users get a ring, mouse clicks do not. Text inputs still ring on click, because browsers treat their focus as visible. - Modals are keyboard-safe —
role="dialog",aria-modal, a name taken from the header, focus moved in and trapped, the rest of the pageinert, and focus returned to the trigger on close. - Dropdowns are navigable —
aria-haspopup/aria-expanded,role="menu"/menuitem, and Arrow / Home / End / Escape / Tab. prefers-reduced-motionis honoured — animations and transitions are neutralised; looping text effects are switched off.
Label icon-only controls with .retro-sr-only, and use
.retro-sr-only-focusable for a skip link:
<button class="retro-btn">💾<span class="retro-sr-only">Save</span></button>
Customization
Override SASS variables or CSS custom properties to customize RetroCSS for your brand or project. See src/scss/_variables.scss for all available variables.
:root {
--retro-primary: #ff00cc;
--retro-bg: #222233;
--retro-font: 'Comic Sans MS', cursive, sans-serif;
}
Corner radius
Every component routes its corners through one token, so a single
declaration rounds the whole framework. It is 0 by
default, which is what makes the chrome look like Windows.
:root { --retro-border-radius: 6px; }
Two components keep their shape deliberately:
.retro-nav-pills and .retro-tag. Tables stay
square because border-collapse: collapse — what
merges their cell borders into a single hairline — makes every
engine ignore border-radius. Radio buttons and the
spinner are circles, as they are in Windows.
For one element at a time, use .retro-rounded,
.retro-rounded-lg or .retro-rounded-full.
Lighting tokens
Three tokens are fixed in both themes because they model a light
source rather than a surface colour. Use them for anything of that
kind: --retro-black-rgb and
--retro-white-rgb invert, which turns a drop shadow into
a glow and a backdrop into a spotlight.
--retro-shadow-rgb /* drop shadows */
--retro-scrim-rgb /* modal backdrops */
--retro-sheen-rgb /* gloss highlights */
Right-to-left
Set dir="rtl" and the layout mirrors. Spacing, text
alignment and start/end positioning are written with logical
properties, so there is nothing to import and no separate stylesheet.
<html lang="ar" dir="rtl">
The bevels deliberately do not mirror. In the Win9x visual language the light source is fixed at the top-left, and Windows itself keeps it there in RTL. Flipping the raised and sunken edges would make every button read as sunken on the wrong side, so only the semantic accents move — an alert's stripe, a blockquote's rule — because those mark where a line of text begins.
See the right-to-left example.
FAQ
Is it responsive?
Yes. Every page in the repository is checked at 1200, 980, 760, 420 and 360 px in both themes, and horizontal overflow fails the build.
Does it support dark mode?
Yes, and it follows the operating system by default. The dark palette
is emitted under prefers-color-scheme as well as
[data-theme="dark"], so a dark-OS visitor is painted dark
on the first frame with no flash. Using the theme toggle stores an
explicit choice, which then outranks the OS.
Can I use it with React, Vue or Svelte?
Yes. The package ships ESM, CJS and TypeScript definitions, and the bundle is safe to import during server-side rendering. See Installation.
Is it accessible?
Every text and surface pair clears WCAG AA in both themes, and every control the framework drives from script is reachable by keyboard and carries an accessible name. Both are enforced in CI rather than asserted here — see Accessibility.
Can I make it less square?
Set one token: --retro-border-radius. See
Customization.
How big is it?
About 22 KB of CSS and 8 KB of JavaScript, gzipped, with no runtime dependencies. The JavaScript is optional — the components are CSS-only until you need the interactive ones.