Using Only CSS to Recreate Windows 98
📣 Sponsor
As part of my continuous work to see how much I can do with just CSS (see other work such as the CSS only Minecraft Chicken), I decided to try and recreate Windows 98 using nothing else apart from CSS and HTML. Did anyone ask for this? Not really? Is it fun to try and see what you can accomplish with just CSS? Yes, sort of. Was it time consuming? Unfortunately yes.
Here is the demo - and a quick note - this is a desktop Windows recreation, so it is of course optimised for desktop viewing. Ironically, though, it probably won’t work on Windows 98, since you would have to use a very old version of Internet Explorer to view it.
See the Pen CSS Only Windows 98 by smpnjn (@smpnjn) on CodePen.
The link to the demo can be found here, since it is better viewed in full screen mode. In this demo the things which I thought were cool included:
- Minesweeper with just CSS - although no score keeping.
- Login and logout with memory of who is logged in using the brand new CSS parent selector.
- Animated update process.
- Minimising, maximising and closing windows.
It is however worth noting that the things that are hard or just down right impossible to do with only CSS, such as:
- Drag and drop - not possible at all with CSS.
- Multiple conditions - for example, hard to say that a window should be both maximised and put on top at the same time.
- Click anywhere to close - you have to click the start button to close and open it. You can’t just click anywhere else to close it.
- Multiple conditions in general - CSS doesn’t really have an AND operator.
How to build Windows 98 in CSS
So, the first thing I wanted to get right about this version of Windows 98 was the look and feel. I’m using some pretty cool Windows 98 icons (which I do think should make a comeback), as well as the standard Windows 98 colour scheme. To get the indented and out-dented feel I used quite a complicated box shadow, as you can see here:
.windows-box-shadow, .minesweeper .content > label {
box-shadow: -2px -2px #e0dede, -2px 0 #e0dede, 0 -2px #e0dede, -4px -4px white, -4px 0 white, 0 -4px white, 2px 2px #818181, 0 2px #818181, 2px 0 #818181, 2px -2px #e0dede, -2px 2px #818181, -4px 2px white, -4px 4px black, 4px 4px black, 4px 0 black, 0 4px black, 2px -4px white, 4px -4px black;
}
.inverse-windows-box-shadow, .minesweeper .content > label:active {
box-shadow: -2px -2px #818181, -2px 0 #818181, 0 -2px #818181, -4px -4px black, -4px 0 black, 0 -4px black, 2px 2px #e0dede, 0 2px #e0dede, 2px 0 #e0dede, 2px -2px #818181, -2px 2px #e0dede, -4px 2px black, -4px 4px white, 4px 4px white, 4px 0 white, 0 4px white, 2px -4px black, 4px -4px white;
}
Everything else was relatively straightforward look and feel wise. The key to making all of this work is checkboxes and radio buttons.
Using Checkboxes and Radio buttons as a store of information in CSS
Checkboxes and radios are the only way to store information in CSS. We can then use them to implement style changes. Checkboxes, when checked, allow us to enable or disable a single feature (like show a window, maximise a window, or click a minesweeper square). For things where there is only one option allowed to be active at a time (for example, which window should be on top) - we can use radio buttons. Both follow the same syntax in CSS, where we use the :checked
selector:
#windows-11:checked ~ .windows-11 .text {
/* -- CSS here -- */
}
Here, when the input #windows-11
is checked, it will affect its sibling’s child .text
- so we can apply some custom CSS. Importantly, since we can’t easily style an HTML input, we use label
s to model the different features of Windows 98. For example:
<form id="windows">
<!-- Login and Shutdown -->
<input type="checkbox" id="login-screen-input" name="login-screen-input" />
<!-- Later on.. -->
<label for="login-screen-input">Log Off</label>
</form>
Here, the label
shown is associated with the checkbox #login-screen-input
. That means when you click the label, it will check the checkbox. This basically gives us free reign to track a user’s clicks, and then use the checkbox :checked
status to show certain windows, in certain forms. The difficulty is you can only have one label associated with one input.
That means in a scenario where a button is supposed to open the window, and place it on top of all other windows, you’d have to use Javascript, since this will require tracking two states - the z-index
of the window, and whether it is open or closed. This is a major blocker in implementing CSS only versions of complex UIs.
Using the parent selector to track who is logged in
Since we have a login screen in a div
for when a user logs out, we can’t use sibling selectors to easily track who is logged in. We can still use :checked
statuses to track this, but the inputs are too deep in our DOM to affect their parent’s sibling’s CSS. Fortunately, we can use the new CSS parent selector for just this task:
#login-screen:has(#login-window .select-box #zark-muckerberg:checked) ~ #start-bar .zark-muckerberg,
#login-screen:has(#login-window .select-box #donald-trump:checked) ~ #start-bar .donald-trump,
#login-screen:has(#login-window .select-box #spiderman:checked) ~ #start-bar .spiderman {
display: inline;
padding-left: 0.5rem;
}
Here, if #login-screen
has a :checked
div, we can use it to display the user name in the start bar, despite these checkboxes being deep within the DOM. This is pretty neat, and a useful way to use parent selectors if you ever wish to accomplish recreating a CSS only version of a Windows operating system.
There is no CSS AND Selector
Much to my dismay, there was no way for me to create a CSS AND
selector using chained checked boxes. For example, consider this situation where we apply some CSS based on a :checked
state:
#minesweeper-box-1-1:checked ~ .content > .minesweeper-box-1-1 {
}
This works fine, but what if we want to check if two minesweeper boxes next to each other are checked, before applying CSS? I thought, logically, that the selector would only continue if both were checked - so tried this:
#minesweeper-box-1-1:checked + #minesweeper-box-2-1:checked ~ .content > .minesweeper-box-1-1 {
}
But unfortunately, that doesn’t work. So while we have a way to track state in CSS, it’s quite hard to track multi-conditioned checkbox states to create logical statements and styles based on that. That’s disappointing, but it doesn’t limit us too much for our Windows 98 implementation.
Achieving Windows 98 Text
Windows 98 text is not anti-aliased. To remove anti aliasing (at least for some browsers), and achieve that classic, crisp, Windows 98 finish, I used the following CSS:
body {
-webkit-font-smoothing: none;
-moz-osx-font-smoothing: grayscale;
}
Recreating Minesweeper
So one of the major undertakings in this project was recreating Minesweeper. I kept the grid relatively small (to maintain my sanity) - but I had to make my own Minesweeper map created out of labels. Each of these labels mapped to an input, which tracked if a cell had been clicked or not. If a mine is clicked, it’s game over and you can’t interact with the board anymore. As there were ~56 Minesweeper cells, we needed an equivalent ~56 Minesweeper inputs. Tracking that all in CSS required a lot of CSS, but the overall result is pretty cool looking.
Overall, this follows the same logic as the previously mentioned checkbox and radio trick - so conceptually it’s not any more complicated than anything else we’ve done.
Conclusion
I hope you’ve enjoyed this guide. Doing this reminded me of how web development used to be, when things were a lot harder to accomplish and required a lot of manual creation of DOM elements. It’s fun to see what can be achieved in CSS all these years on (including the parent selector). Is this a realistic way to create web applications? Not really in terms of speed, and not yet in terms of functionality, but CSS did a lot more than I thought it would be able to, and I’m pretty happy with the results.
If you enjoyed this, please consider following me on twitter.