Vintage Terminal Effect in CSS3

Posted on by in Development

Recently I revamped my personal website. For the most part I kept it simple, having it generated by Middleman with styling and layout provided by Bootstrap. However, I wanted my header to reflect my love of the command-line and 80s nostalgia by having it act like a vintage terminal similar to Cathode app: scanlines, screen burn, green glow, with blinking cursor. Most importantly, I wanted to do it with CSS3 animations and no Javascript to minimize the impact on browsers. Building on the work of Lea Verou and Anders Evenrud, here’s how I did it.

Typing and Blinking in steps()

First thing I wanted was to have my name be “typed” out in the header on page load by a blinking cursor. Lea Verou covered exactly how to pull this off. It’s basically a “sliding door” effect where an element initially completely covering the text has its width shrunk to slowly reveal the content. The animation happens in discrete steps (literally using the steps() CSS3 timing function) so that after each step only one character is revealed. Every step will decrease the width by the same amount, so to ensure only a single character is revealed at a time a monospaced font (where every character has the same fixed width) will be used. Finally, the blinking block cursor is achieved by setting the covering element’s left border to that same fixed width and animating it’s color.

Let’s see the code.

The text is wrapped in an element (an anchor tag in my case) and a span playing the role of the covering element described above is appended with an empty space.

<a href="">Rudy Jahchan <span class="cursor"> </span></a>

In the CSS, the anchor is set to have the monospace font and the span is absolute-ly positioned, anchoring it to the top, bottom, and left of its containing element. This is done so when the width changes, it shrinks towards the right side.

a {
    position: relative;
    font-family: monospace;
    background-color: black;
    text-decoration: none;
    color: green;
    font-size: 1.5em;

.cursor {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    width: 0;
    background; black;

A CSS3 animation is defined for the beginning and end state of the typing. At the start the span 100% matches the width of its parent to completely hide the text. The animation ends with the width set to 0 so that all the text is revealed (again, it’s a sliding door).

/* Need to add prefix variants */
@keyframes typing {
    from { width: 100%; }
    to { width: 0; }

Then the animation is applied to the cursor with an appropriate duration and made to repeat infinite-ly.

.cursor {
  // ...

  // Need to add vendor prefix variants
  animation: typing 6s infinite;

At this point the animation smoothly reveals the text.

We now add the steps() call, one for each character.

.cursor {
  // ...

  // Need to add vendor prefix variants
  animation: typing 6s steps(13, end) infinite;

Now for the blinking cursor. As described before, the cursor is actually the left border of the span, its size set to match a single character width of the chosen (again, monospaced!) font. This is the one thing I had to experiment with to get right as it can vary depending on the font used. Also, it’s very important that the box-sizing strategy of the element is set to border-box to ensure the border is drawn within the span element.

.cursor {
    // ...

    // Need to add vendor prefix variants
    box-sizing: border-box;
    border-left: .5em solid;

Another animation is defined for the blinking that takes the border color from transparent to the cursor color and back.

@keyframes blinking {
    from, to { border-color: transparent; }
    50% { border-color: green; }

This animation is applied to the same span and set to toggle to step to the end state (step-end) within 1 second repeatedly.

.cursor {
  // ...

  // Need to add vendor prefix variants
  animation: typing 6s steps(13, end) infinite, blinking 1s step-end infinite;

And voila, my name is typed out with a blinking cursor.


The next task was to get that glorious old school scan-line feel. Anders Evenrud created and amazing demo of a vintage terminal which I borrowed liberally from. The technique consists of structuring a number of div elements and their :before and :after pseudo elements to produce the entire effect.

<div id="faux-terminal">
  <div class="layer"></div>
  <div class="overlay"></div>

First there is the outer “screen” layer of faux-terminal. The fixed scanlines are drawn by styling the :before pseudo element to fill it’s background with a linear gradient. However, it also declares it’s background size as being only 4px size and repeating, thus creating the lines! The whole thing is given a high z-index to float over everything and a subtle pulse animation.

#faux-terminal:before {
  // ... positioning
  z-index: 4010;
  background: linear-gradient(#444 50%, #000 50%);
  background-size: 100% 4px;
  background-repeat: repeat-y;
  opacity: .14;
  box-shadow : inset 0px 0px 1px 1px rgba(0, 0, 0, .8);
  animation: pulse 5s linear infinite;

@keyframes pulse {
  0%   {transform: scale(1.001);  opacity: .14; }
  8%   {transform: scale(1.000);  opacity: .13; }
  15%  {transform: scale(1.004);  opacity: .14; }
  30%  {transform: scale(1.002);  opacity: .11; }
  100% {transform: scale(1.000);  opacity: .14; }

The :after pseudo element then adds a light “sheen” through a radiant gradient and box-shadow, the latter inset to have the effect within the element’s boundaries.

#faux-terminal:after {
  // ... positioning
  z-index : 4011;
  background-color : $rudy-accent-color;
  background: radial-gradient(ellipse at center, rgba(0,0,0,1) 0%,rgba(0,0,0,0.62) 45%,rgba(0,9,4,0.6) 47%,$rudy-accent-color 100%);
  box-shadow : inset 0px 0px 4px 4px rgba(100, 100, 100, .5);
  opacity : .1;

Next up is another layer for the actual green burn, achieved by a radient gradient from our terminal green to a dark grey and it’s own subtle glitching animation.

.layer {
  // ... positioning
  z-index : 4001;
  box-shadow : inset 0px 0px 1px 1px rgba(64, 64, 64, .1);
  background: radial-gradient(ellipse at center,darken($rudy-accent-color,1%) 0%,rgba(64,64,64,0) 50%);
  transform-origin : 50% 50%;
  transform: perspective(20px) rotateX(.5deg) skewX(2deg) scale(1.03);
  animation: glitch 1s linear infinite;
  opacity: .9;

.layer:after {
  // ... positioning
  background: radial-gradient(ellipse at center, rgba(0,0,0,0.5) 0%,rgba(64,64,64,0) 100%);
  opacity: .1;

@keyframes glitch {
  0%   {transform: scale(1, 1.002); }
  50%   {transform: scale(1, 1.0001); }
  100% {transform: scale(1.001, 1); }

Finally, we animate the scanline with an overlay whose :before pseudo element is fixed 5px tall and animated to repeatedly slide down.

.overlay {
  // ... positioning
  z-index: 4100;

.overlay:before {
  content : '';
  position : absolute;
  top : 0px;
  width : 100%;
  height : 5px;
  background : #fff;
  background: linear-gradient(to bottom, rgba(255,0,0,0) 0%,rgba(255,250,250,1) 50%,rgba(255,255,255,0.98) 51%,rgba(255,0,0,0) 100%); /* W3C */
  opacity : .1;
  animation: vline 1.25s linear infinite;

.overlay:after {
  // ... positioning
  box-shadow: 0 2px 6px rgba(25,25,25,0.2),
              inset 0 1px rgba(50,50,50,0.1),
              inset 0 3px rgba(50,50,50,0.05),
              inset 0 3px 8px rgba(64,64,64,0.05),
              inset 0 -5px 10px rgba(25,25,25,0.1);

@keyframes vline {
  0%   { top: 0px;}
  100% { top: 100%;}

Now, here is where my implementation differs from Anders Evenrud’s. His layer structures wrap a text area containing the “output” of his virtual console. In my case, however, I wanted this effect to go over a Bootstrap navigation bar component. I did not want to go down the rabbit hole of restyling or redefining the component and decided to (somewhat hackely) absolute-ly position all the terminal layers over the toolbar. Since I was using SASS it was easy enough to make a mixin to help DRY-up the code.

However, I ran into a huge problem. The virtual screen blocked all interactions with the navigation bar! This was easily remedied by setting the pointer-events property of the screen layer to none. All events are now passed through to the navigation bar.

Whew! As you can see, a lot of CSS3 properties and functions were used along the way. While most are fully supported by all the major browsers some still require [vendor prefixing][], as noted in some of the sample code. Maintaining this long-term can be frustrating as the slightest change will need to be replicated. SASS mixins can alleviate some of these concerns but even better is autoprefixer. It parses your CSS and adds the necessary prefixes. Obviously you would not want to do this for every request in production but it’s perfect for static site generation and a snap to integrate.

Final Thoughts

One of the reasons I went with an all CSS3 solution was to avoid having an ongoing Javascript process driving the animation and taking up resources. That said, I want to investigate what is the impact of all these effects on performance.

Regardless, it was great fun to implement and has me wanting to tackle other great 80s interfaces. Maybe the motion tracker from Alien or the Blade Runner zoom-and-enhance?