diff --git a/src/index/index.js b/src/index/index.js
index 9dd5dd1..112bc14 100644
--- a/src/index/index.js
+++ b/src/index/index.js
@@ -1,2 +1,115 @@
 import "bootstrap";
 import "./index.scss";
+
+// Helpers
+
+/**
+ * "Types" out a DOM element, emulating the way a human might.
+ */
+class Typer {
+    /**
+     * Create the typer.
+     * @param {HTMLElement} element - The element to type.
+     * @param {number} blink - The time between cursor blinks.
+     * @param {number} blink_timeout - How long the cursor should keep
+     *                                 blinking for after the text
+     *                                 finishes typing.
+     */
+    constructor(element, blink, blink_timeout) {
+        // Retrieve the current content and wipe it. We also make the
+        // element visible if it was hidden.
+        this._element = $(element);
+        this._text = this._element.html();
+        this._element.html("");
+        this._element.css("visibility", "visible");
+
+
+        this._cursor = false;
+        this._typed = 0;
+
+        this._min = 20;
+        this._max = 70;
+        this._blink_tick = blink;
+        this._blink_timeout = blink_timeout;
+
+        this._end = null;
+    }
+
+    /**
+     * Start typing.
+     */
+    type() {
+        this._type();
+        this._blink();
+    }
+
+    /**
+     * Draw the current text line, i.e., anything that has been typed
+     * so far, and a cursor if it is currently supposed to be on.
+     * @private
+     */
+    _draw() {
+        let text = this._text.slice(0, this._typed);
+
+        if (this._cursor)
+            text += "\u2588";
+
+        window.requestAnimationFrame(() => this._element.html(text));
+    }
+
+    /**
+     * Type the next character, and prepare to draw the next one. If
+     * no new characters are to be drawn, set the end timestamp.
+     * @private
+     */
+    _type() {
+        this._typed += 1;
+        this._draw();
+
+        if (this._typed != this._text.length)
+            setTimeout(this._type.bind(this), this._type_tick());
+        else
+            this._end = Date.now();
+    }
+
+    /**
+     * Make the cursor change blink status, and prepare for the next
+     * blink.
+     * @private
+     */
+    _blink() {
+        this._cursor = !this._cursor;
+        this._draw();
+
+        // As long as we are typing, keep blinking
+        if (this._typed != this._text.length)
+            setTimeout(this._blink.bind(this), this._blink_tick);
+
+        // Once typing ends, keep going for a little bit
+        else if (Date.now() - this._end < this._blink_timeout)
+            setTimeout(this._blink.bind(this), this._blink_tick);
+
+        // Make sure we get rid of the cursor in the end
+        else {
+            this._cursor = true;
+            setTimeout(this._blink.bind(this), this._blink_tick);
+        }
+    }
+
+    /**
+     * Calculate a "human" time for the next character to type.
+     * @private
+     */
+    _type_tick() {
+        return Math.round(Math.random() * this._max) + this._min;
+    }
+}
+
+// Application code
+function main() {
+    // Type the head line
+    let typer = new Typer($(".head-line .typed").get(0), 500, 3000);
+    typer.type();
+}
+
+$(document).ready(main);
diff --git a/src/index/index.pug b/src/index/index.pug
index 2975f60..7553014 100644
--- a/src/index/index.pug
+++ b/src/index/index.pug
@@ -1,7 +1,15 @@
 extends ../lib/pug/base
 
 block content
-  h1.head-line Welcome to tlater.net!
+  noscript
+    style.
+      .head-line .typed {
+        visibility: visible;
+      }
+
+  h1.head-line
+    | $&nbsp;
+    span.typed Welcome to tlater.net!
   h5.tag-line: em Your #1 site for useless time sinks
 
   hr
diff --git a/src/index/index.scss b/src/index/index.scss
index 5572cae..03b07ff 100644
--- a/src/index/index.scss
+++ b/src/index/index.scss
@@ -1,2 +1,6 @@
 @import "../lib/scss/main";
 @import "~bootstrap/scss/bootstrap";
+
+.head-line .typed {
+  visibility: hidden;
+}