RetroCSS — Documentation

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

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.

TokenRole
--retro-primaryFill. Background of a filled badge, button or alert.
--retro-primary-fgOn-fill. Text placed on that fill.
--retro-primary-textOn-surface. That hue used as text on a page background.
--retro-primary-hover / -activeStates. 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…8 and retro-p…, plus retro-gap-0…8 and retro-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-saferole="dialog", aria-modal, a name taken from the header, focus moved in and trapped, the rest of the page inert, and focus returned to the trigger on close.
  • Dropdowns are navigablearia-haspopup/aria-expanded, role="menu"/menuitem, and Arrow / Home / End / Escape / Tab.
  • prefers-reduced-motion is 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.