Bob’s Blog:

Simulating a CRT Monitor in CSS

September 7, 2020 ⁓ 8 min read

Introduction

For a future project I want to simulate a CRT monitor for that nostalgic look. Back in the day, I had a Hazeltine Esprit terminal in my bedroom and worked with various other terminals at school and work. There was just something about that green glow that I miss. Let me see if I can recreate it.

So where to begin? Do I see what I can come up with? No way. I’m a programmer so that means that I am naturally lazy. Why reinvent the wheel if someone else has already done it?

I start my search looking for something I like. After a bunch of noes, I come across a demo page by Anders Evenrud. Wowsa! Here is his blog post describing it. I found my starting point. Anders went for hyperrealism. Having used dumb terminals for over 20 years, this terminal looks like something that was pulled out of the trash with its glitchy picture, rolling interference, and faded, out of focus tube. A little too much for me. I just want to give the feel of an old-time CRT without complete realism.

Using Anders’ code as a starting point, I started removing things. The animation had to go, it annoyed me too much. I took out some of the picture tube distortion and the scan lines. Again, I don’t need it too realistic and it allowed me to simplify the markup a bit. I also parameterized the settings with CSS custom properties (a.k.a., CSS variables).

The markup

The markup is pretty straightforward: just three nested divs containing the content.

<div class="crt-bezel">
  <div class="crt">
    <div class="crt-scan-area">
<em> West of House                                      Score: 3     Moves: 17     </em>
You are in an open field west of a big white house with a boarded
front door.

There is a small mailbox here.

>OPEN MAILBOX

Opening the mailbox reveals:
A leaflet.

>APPLY THE BRAKES
The Frobozz Magic Go-Cart coasts to a stop.

Moss-Lined Tunnel, in the Go-Cart
This is a long east-west tunnel whose walls are covered with green and yellow mosses.
There is a jewel-studded monkey wrench here. (outside the Go-Cart)
A bent and rusty monkey wrench is lying here. (outside the Go-Cart)

>TAKE THE WRENCH
Which wrench do you mean, the jeweled monkey wrench or the rusty monkey wrench?

>JEWELED
You can't reach it from inside the Go-Cart.

>WEST
You're not going anywhere until you stand up.
    </div>
  </div>
</div>

The CSS

Before getting into the CSS itself, here are the custom properties. First, the phosphor colors:

$green-phosphor: #3f3; // P1
$amber-phosphor: #ffb000; // P3
$white-phosphor: #cce;
$phosphor-color: $amber-phosphor;

See them in action:

Here is the what was then the more common green:

And last, but least (in my opinion), white phosphor:

The remaining custom properties:

$crt-bg-color: #141414;
$crt-margin: 1em;
$crt-font-size: clamp(6px, 1.5vw, 20px); // these values need to be hand-tuned
$crt-line-height: 1.33333;
$crt-border-radius: 1.33333em;
$crt-char-width: 80; // add 1 if using vertical scroll bars
$crt-lines: 24; // number of lines or auto for full height
$crt-overflow-y: hidden; // auto or hidden (scroll bars or no)
$crt-bezel-width: 1.66667em;
$crt-overscan-width: 2.66667em;

Description

$crt-bg-color
The background color of the CRT. They were not really black. Someone suggested it should be #282828, but I found that too light.
$crt-margin
The margin outside the bezel. If you do not want it the same all the way around, set your values in appropriate place. This value must be whatever the top and bottom margin are for the height calculation to work.
$crt-font-size
The font size of the text. All of the sizing calculations are based off of this value (hence, all of the em values in the CSS). I played around with having the CRT size scale with the browser width (1.5vw) and have a minimum and maximum value (the clamp() function). Change this to your liking: fixed size, media breakpoints, whatever.
$crt-line-height
Line height of the text, obviously. I know that CRTs had pretty low line heights, but this seemed reasonable. I think 1.1 is a more realistic value, but I went for better legibility.
$crt-border-radius
The border radius of the bezel.
$crt-char-width
Character width of the terminal. 80 was typical, the TRS-80 Model I was 64, some terminals had a 132-character mode. Add 1 if you are using vertical scroll bars and want your full character width.
$crt-lines
The number of lines on the CRT. Set it to auto to display the CRT full screen height.
$crt-overflow-y
Set to auto to display a scroll bar or hidden to hide overflow.
$crt-bezel-width
The width of the bezel.
$crt-overscan-width
The text on a CRT did not go all the way to the edge. This sets the padding between the bezel and the text.

Here is the CSS for the bezel:

.crt-bezel {
  display: inline-block;
  position: relative;
  margin: $crt-margin;
  box-shadow: inset 0 0 1px 0.66667em #000;
  border-radius: $crt-border-radius;
  background: #1D1D1D;
  overflow: hidden;
  font-size: $crt-font-size;
  line-height: $crt-line-height;

  &:before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 2;
    background: linear-gradient(135deg, rgba(149,149,149,0.5) 0%,rgba(13,13,13,0.55) 19%,rgba(1,1,1,0.64) 50%,rgba(10,10,10,0.69) 69%,rgba(51,51,51,0.73) 84%,rgba(22,22,22,0.76) 93%,rgba(27,27,27,0.78) 100%);
    opacity: .5;
  }

  &:after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    background-color: #ddd;
    opacity: .1;
  }
}

The CSS for the CRT:

.crt {
  position: relative;
  margin: $crt-bezel-width;
  border-radius: $crt-border-radius;
  box-shadow: 0 0 1px 3px rgba(10, 10, 10, .7);
  background: $crt-bg-color;
  overflow: hidden;
  z-index: 3;

  &:before {
    content: "";
    position: absolute;
    inset: 0;
    box-shadow: inset 0 0 5px 5px rgba(255, 255, 255, .1);
    border-radius: $crt-border-radius;
    background: radial-gradient(ellipse at center, rgba($phosphor-color, 0.1) 0%, rgba(255,255,255,0) 100%);
    z-index: -1;
  }
}

The only thing of note here is the overlay that gives the screen a slight phosphor glow — a nice touch of realism without going overboard.

Finally, the CSS for the scan area (the text):

.crt-scan-area {
  width: #{$crt-char-width * 1ch};
  @if $crt-lines == 'auto' {
    height: calc(100vh - #{$crt-margin * 2} - #{$crt-bezel-width * 2} - #{$crt-overscan-width * 2});
  } @else {
    height: ($crt-lines * 1em * $crt-line-height);
  }
  //scrollbar-width: none; // Maybe?
  margin: $crt-overscan-width;
  overflow-y: $crt-overflow-y;
  white-space: pre-wrap;
  font-family: monospace;
  color: $phosphor-color;
  text-shadow: 0 0 1px rgba($phosphor-color, .8);

  em {
    color: $crt-bg-color;
    background-color: $phosphor-color;
    font-style: normal;
  }
}

When $crt-lines is set to auto, the height is set to the browser view height minus the top and bottom margin, minus the top and bottom bezel width, minus the top and bottom CRT overscan width. Notice the text shadow gives the text a slight glow effect. Without it, the text is too crisp and does not look like an old CRT.

The em element is used to set reverse-video text. (I do not remember any dumb terminals having italic characters.)

The result

Here is the final result with $crt-overflow-y set to auto:

Starting with a hyperrealistic CRT simulation from Anders Evenrud, I toned down the realism and added some options to get a nice reminiscence of a CRT. Enjoy. Feel free to offer any changes or ideas through codepen.

Credits: DEC VT100 terminal image by Jason Scott / CC BY

Tags