98.css: Build Windows 98-Style Interfaces with Semantic HTML
Picture this: you’re building a small web app and you want it to feel instantly recognizable. Not “modern minimal”—more like that satisfying, slightly chunky Windows 98 look: bevels, sunken panels, dotted focus rectangles, and buttons that look like they belong on a desktop from a different era.
That’s exactly where 98.css comes in. It’s a CSS library (a set of styles) designed for building faithful recreations of old Windows user interfaces, especially Windows 98, without turning your interface into an unmaintainable pile of custom CSS. (jdan.github.io)
What 98.css is (and what it isn’t)
At a high level, 98.css is “design-system thinking” applied to a specific aesthetic. Instead of free-form styling, it maps old-school UI controls to HTML elements you already know.
Two ideas shape the whole project:
- Semantic HTML. “Semantic” means the HTML describes the meaning of the content. A real
<button>is a button. A real<fieldset>groups related options. This is not just pedantry—screen readers and keyboard navigation depend on it. (jdan.github.io) - No JavaScript required. 98.css is meant to be CSS-only: it styles your HTML and lets your own framework (or plain HTML) handle behavior. (jdan.github.io)
That leads to a very practical question for anyone starting out: Why do people build retro UI libraries around semantic HTML instead of drawing everything with div elements? Because the browser already understands the controls. 98.css leans on that understanding.
Installing 98.css: the fastest path
If you want the “try it right now” experience, import the stylesheet from a CDN. The official docs show this pattern: (jdan.github.io)
<link rel="stylesheet" href="https://unpkg.com/98.css">
Or, if you prefer Node tooling and bundlers, 98.css can also be installed via npm. (jdan.github.io)
npm install 98.css
Because this is CSS-only, this library works with whatever you’re already using: vanilla HTML, React, Vue, Svelte—anything that outputs HTML.
The big “aha”: use real form controls
98.css isn’t a bag of “pretty divs.” It expects you to use actual form elements so it can apply Windows 98 styling to the right places.
For example:
- A clickable command button is a real
<button>. - A checkbox should be an
<input type="checkbox">paired with a<label>. - Radio-style mutually exclusive options should use
<input type="radio">with matchingnamevalues. - Icon-only buttons (when you have no visible text) should be labeled with
aria-label.
That labeling requirement isn’t optional busywork. It’s what makes the UI usable when people navigate with a keyboard or screen reader, and it also improves normal usability (you can click the label to toggle the checkbox).
Buttons: bevels, pressed state, and focus
A standard 98.css button uses a classic “raised outer and inner border” look, plus a little horizontal padding. The interesting part is that 98.css also styles interaction states—pressed/active, disabled, and focus—so the UI feels alive even with no JavaScript.
Here’s the basic structure:
<button>Click me</button>
<input type="submit" value="Submit">
<input type="reset" value="Reset">
A couple of small upgrades matter:
- Add the class
defaultto show an “Enter key” default action, matching Windows 98 behavior. - When a button is disabled (
disabledattribute), the label looks washed out. - Focus is shown with a dotted rectangle inset inside the button area.
Even if you never “notice” focus styling while building, users notice it constantly once they rely on keyboard navigation.
Checkboxes: the label is part of the control
A checkbox is an independent choice: it can be on or off, regardless of other checkboxes. In Windows-era UI terms, 98.css draws a sunken panel and shows a check mark when selected.
The key pattern is: the <label> must come after the checkbox input, and the label’s for must point at the checkbox id. That’s what binds the two together for assistive tech and for the whole “click the text to toggle” usability benefit.
Example:
<div class="field-row">
<input type="checkbox" id="example1">
<label for="example1">This is a checkbox</label>
</div>
For spacing consistency, 98.css also uses a helper class called field-row when you’re stacking form controls. It gives you predictable gaps between each control-label pair.
You can select or disable checkboxes with standard HTML attributes:
<div class="field-row">
<input type="checkbox" id="example2" checked>
<label for="example2">I am checked</label>
</div>
<div class="field-row">
<input type="checkbox" id="example3" disabled>
<label for="example3">I am inactive</label>
</div>
Radio buttons (option buttons): group with name
Radio buttons are for mutually exclusive choices: you choose one option from a limited set. In HTML, that exclusivity comes from the name attribute.
You still pair each radio <input> with a <label> using id + for.
<div class="field-row">
<input id="radio5" type="radio" name="first-example">
<label for="radio5">Yes</label>
</div>
<div class="field-row">
<input id="radio6" type="radio" name="first-example">
<label for="radio6">No</label>
</div>
To mark a default choice, use checked. To disable the whole option, use disabled.
And again, field-row keeps the vertical rhythm consistent.
Group boxes: wrap controls with <fieldset> and <legend>
A group box in Windows UI terms is a framed region that collects related controls. In HTML, the native match for this concept is <fieldset>.
- Wrap the group in
<fieldset>. - Add a
<legend>for the group’s label.
A simple example:
<fieldset>
<legend>Today's mood</legend>
<div class="field-row">
<input id="radio13" type="radio" name="mood">
<label for="radio13">Claire Saffitz</label>
</div>
<div class="field-row">
<input id="radio14" type="radio" name="mood" checked>
<label for="radio14">Brad Leone</label>
</div>
</fieldset>
98.css styles the fieldset into that sunken/raised engraved look, so you get the “Windows frame” without building one out of nested divs.
Beyond forms: Windows-style layout pieces
Once you start treating 98.css as a set of “UI primitives” rather than a random theme, other components click into place.
For instance, 98.css can style list-based structures like a TreeView (a hierarchical outline) using a ul with the tree-view class, and it can build tab-like navigation using ARIA roles on a menu/list structure.
It also supports window-like scaffolding:
- A container with a
windowclass - A
title-bar - A
window-body - Optional pieces like a
status-barthat displays context text at the bottom of the window
That’s how 98.css manages to feel coherent: instead of inventing a proprietary component system, it grows out of standard HTML patterns.
Overriding styles without losing the vibe
Because 98.css applies a themed look through CSS classes and element selectors, you can override many parts safely.
If you need more padding on a button, you can add your own CSS rule targeting button inside your window or app scope. If you need to adjust spacing between form rows, you can modify the field-row class.
The mental model is: let 98.css handle “bevel logic” (raised vs sunken, focus indicators, disabled appearance), and override only what’s specific to your layout.
This is why the “no JavaScript” approach matters: the visuals come from CSS states and semantic element structure, not from runtime scripting.
A practical takeaway
98.css is fun, but it’s also educational. It shows a design-system lesson that modern UI teams sometimes forget: you can get consistent, accessible UI styling when you start with the correct HTML controls.
So if you’re building a retro-themed interface—or just experimenting with better semantic markup—98.css gives you a ready-made, battle-tested visual language. And the best part is that the code stays ordinary HTML, with CSS doing the storytelling. (jdan.github.io)
Comments (0)
No comments yet. Be the first to respond!
Leave a Comment
Your comment will be visible after review.