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.
As a quick note - this is a desktop Windows recreation, so it is of course optimized 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.
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 scorekeeping.
Login and logout with the memory of who is logged in using the brand new CSS parent selector.
Animated update process.
Minimizing, maximizing, and closing windows.
It is however worth noting that the things that are hard or just downright impossible to do with only CSS, such as:
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 color 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.
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 showing a window, maximizing a window, or clicking 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.
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.
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 states 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.
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;
}
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 of ~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.
I hope you've enjoyed this guide. Doing this reminded me of how web development was about 10 years ago 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.
Also published here.