[X]

</> DHTML Snippets

239 pieces of obsolete, non-standard, plainly weird markup from the old web · 129 still work · 50 partly · 60 dead

Every piece here is a real exhibit, pulled from the old web with its original code intact, with a note on what a browser does with it in 2026 and a line on where it came from. Press run it to execute one, copy to grab the source, or link to copy a share link. Every snippet also has its own page: click the # beside its title. The dead ones run too. A SPACER tag no browser has honoured since 2011, or a PLAINTEXT tag swallowing the rest of the document, is the whole point, so nothing here is withheld for being broken. The demo frame is sandboxed with no access to this page, its cookies or its storage.

Status Bar Scroller

text effects 1996 dead #

Scrolls a message along the browser status bar at the bottom of the window.

In 2026: Dead. Browsers shut window.status off one by one through the 2000s because it was used to fake link targets: IE7 turned it off by default in 2006 and Chrome never drew it at all. The code still runs, it just has nowhere to draw. The original looped with a string setTimeout, which a Content Security Policy blocks like eval, so the loop here uses a function reference instead.

Where it came from: Netscape JavaScript examples, 1996. Reprinted by every script archive of the period.
MDN HTML standard Wayback: Dynamic Drive

<script language="JavaScript">
<!-- hide from old browsers
var msg = "Welcome to my home page!!!   Thanks for visiting!!!   ";
var pos = 0;
function scrollStatus() {
  window.status = msg.substring(pos) + msg.substring(0, pos);
  pos = (pos + 1) % msg.length;
  setTimeout(scrollStatus, 150);   // the original passed the string "scrollStatus()"
}
scrollStatus();
// end hide -->
</script>

<p>This scrolls a message along the browser's status bar. Modern browsers ignore
   <code>window.status</code>, so the bar at the bottom stays empty and nothing
   appears on screen. The code is running; it just has nowhere to draw.</p>
sandboxed demo · breaks nothing but itselfrestart

Marquee Tag

text effects 1996 partly #

The original scrolling banner. One tag, no script.

In 2026: Still renders in every browser, and formally obsolete: the HTML standard bans authors from writing it and tells browsers to keep rendering it. Use CSS animation if you want it to survive.

Where it came from: Microsoft Internet Explorer 2 HTML extensions, 1996.
MDN HTML standard

<marquee>*** WELCOME TO MY HOMEPAGE *** SIGN MY GUESTBOOK ***</marquee>
sandboxed demo · breaks nothing but itselfrestart

Blinking Text

text effects 1995 partly #

The most hated tag on the web, plus a CSS version that still works.

In 2026: The <blink> tag was removed from every browser. The CSS keyframes version below does the same thing and is what you actually want.

Where it came from: Netscape Navigator 1.0, 1994. Lou Montulli has said it came out of an evening conversation in a bar and was written the same night.
Wikipedia Lou Montulli

<!-- The original (dead everywhere; Firefox was the last to blink it, until 2013) -->
<blink>UNDER CONSTRUCTION</blink>

<!-- The version that still blinks in 2026 -->
<style>
@keyframes blink90s { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }
.blink { animation: blink90s 1s steps(1) infinite; }
</style>
<span class="blink"><b>NEW!</b></span>
sandboxed demo · breaks nothing but itselfrestart

Rainbow Text

text effects 1997 still works #

Colours every letter of a heading a different colour of the rainbow.

In 2026: Works fine. It writes with document.write, which only runs during page load, so keep it inline where you want the text.

Where it came from: Free script archives, late 90s. Too many near-identical copies to trace a first one.
Wikipedia Wayback: JavaScript Source

<script language="JavaScript">
var colours = ["#FF0000","#FF7F00","#FFFF00","#00FF00",
               "#0000FF","#4B0082","#9400D3"];
function rainbow(text) {
  var out = "";
  for (var i = 0; i < text.length; i++) {
    out += '<font color="' + colours[i % colours.length] + '">'
         + text.charAt(i) + '</font>';
  }
  document.write(out);
}
</script>

<h1><script language="JavaScript">rainbow("MY HOME PAGE");</script></h1>
sandboxed demo · breaks nothing but itselfrestart

Typewriter Text

text effects 1998 still works #

Types a message out one character at a time.

In 2026: Works unchanged. The original used document.all, this version uses getElementById so it runs everywhere.

Where it came from: Dynamic Drive and The JavaScript Source, roughly 1998.
MDN Wayback: Dynamic Drive

<span id="typed"></span><span id="caret">_</span>

<script language="JavaScript">
var msg = "Welcome to my corner of the internet...";
var i = 0;
function typeIt() {
  if (i < msg.length) {
    document.getElementById("typed").innerHTML += msg.charAt(i++);
    setTimeout(typeIt, 90);
  }
}
setInterval(function () {
  var c = document.getElementById("caret");
  c.style.visibility = (c.style.visibility === "hidden") ? "visible" : "hidden";
}, 500);
typeIt();
</script>
sandboxed demo · breaks nothing but itselfrestart

Title Bar Scroller

text effects 1997 still works #

Scrolls text across the browser tab title. The status bar trick's surviving cousin.

In 2026: Still works everywhere. This is the one status-bar effect browsers never took away.

Where it came from: Same family as the status bar scroller. Free script archives, 1997.
MDN

<script language="JavaScript">
var tmsg = "*** WELCOME *** THANKS FOR VISITING *** ";
var tpos = 0;
function scrollTitle() {
  document.title = tmsg.substring(tpos) + tmsg.substring(0, tpos);
  tpos = (tpos + 1) % tmsg.length;
  setTimeout(scrollTitle, 200);
}
scrollTitle();
</script>

<p style="font:13px Verdana">The title of this frame's own document is scrolling
right now, but a frame's title never reaches the browser tab, so here is a live
mirror of it. Put the script on a real page and the tab itself scrolls.</p>
<div id="tmirror" style="font:bold 14px 'Courier New';background:#000;color:#0f0;
     display:inline-block;padding:4px 10px"></div>
<script language="JavaScript">
setInterval(function () {
  document.getElementById("tmirror").innerHTML = document.title;
}, 200);
</script>
sandboxed demo · breaks nothing but itselfrestart

Glowing Text

text effects 1999 partly #

Neon glow on a heading. The IE filter original plus the modern equivalent.

In 2026: The IE filter is gone. text-shadow does the same job and works in every browser.

Where it came from: Microsoft Internet Explorer 4 visual filters, MSDN, 1997.
MDN

<!-- Internet Explorer 4 only -->
<span style="filter: glow(color=#00FFFF, strength=6); width: 100%">
  GLOWING
</span>

<!-- The version that still glows -->
<style>
.glow90s {
  color: #fff; font-family: Impact, sans-serif; font-size: 2rem;
  text-shadow: 0 0 4px #0ff, 0 0 10px #0ff, 0 0 22px #08c, 0 0 40px #08c;
}
</style>
<span class="glow90s">GLOWING</span>
sandboxed demo · breaks nothing but itselfrestart

Wavy Text

text effects 1998 still works #

Letters ride up and down on a sine wave.

In 2026: Works. The original positioned each letter with a <layer>, this one uses spans.

Where it came from: Dynamic Drive, roughly 1998. The original positioned each letter with a Netscape LAYER.
Wikipedia

<div id="wavy" style="font: bold 28px Arial; color: #FF00FF"></div>

<script language="JavaScript">
var wtext = "WAVY TEXT!";
var spans = [];
var host = document.getElementById("wavy");
for (var i = 0; i < wtext.length; i++) {
  var s = document.createElement("span");
  s.style.display = "inline-block";
  s.innerHTML = wtext.charAt(i) === " " ? "&nbsp;" : wtext.charAt(i);
  host.appendChild(s);
  spans.push(s);
}
var phase = 0;
setInterval(function () {
  phase += 0.25;
  for (var j = 0; j < spans.length; j++) {
    spans[j].style.transform =
      "translateY(" + (Math.sin(phase + j * 0.6) * 10).toFixed(1) + "px)";
  }
}, 50);
</script>
sandboxed demo · breaks nothing but itselfrestart

Colour Fading Text

text effects 1998 still works #

Cycles a heading smoothly through the colour wheel.

In 2026: Works unchanged.

Where it came from: Free script archives, late 90s. Origin unclear.
MDN

<h2 id="fader" style="font-family: Impact, sans-serif">FADING COLOURS</h2>

<script language="JavaScript">
var hue = 0;
setInterval(function () {
  hue = (hue + 3) % 360;
  document.getElementById("fader").style.color = "hsl(" + hue + ",100%,55%)";
}, 60);
</script>
sandboxed demo · breaks nothing but itselfrestart

Star Mouse Trail

mouse and cursor 1998 still works #

A tail of little stars that chases the pointer around the page.

In 2026: Rewritten from the <layer> original to absolutely positioned divs, so it works in current browsers. The behaviour is identical.

Where it came from: The JavaScript Source and Dynamic Drive, roughly 1998. Dozens of variants circulated at once.
MDN Wayback: Dynamic Drive

<script language="JavaScript">
var TRAIL = 14, dots = [];
for (var i = 0; i < TRAIL; i++) {
  var d = document.createElement("div");
  d.innerHTML = "\u2726";
  d.style.cssText = "position:fixed;left:0;top:0;pointer-events:none;z-index:9999;"
    + "color:hsl(" + (i * 360 / TRAIL) + ",100%,65%);font-size:"
    + (16 - i * 0.8) + "px";
  document.body.appendChild(d);
  dots.push({ el: d, x: 0, y: 0 });
}
var mx = 0, my = 0;
document.onmousemove = function (e) { mx = e.clientX; my = e.clientY; };
setInterval(function () {
  var px = mx, py = my;
  for (var i = 0; i < dots.length; i++) {
    var s = dots[i];
    s.x += (px - s.x) * 0.35;
    s.y += (py - s.y) * 0.35;
    s.el.style.left = s.x + "px";
    s.el.style.top = s.y + "px";
    px = s.x; py = s.y;
  }
}, 25);
</script>
sandboxed demo · breaks nothing but itselfrestart

Text Follows The Cursor

mouse and cursor 1999 still works #

Your message orbits the mouse pointer, one letter per trailing position.

In 2026: Works. This was the single most-copied script on the 1999 web.

Where it came from: Usually credited to Peter Gehrig and Urs Dudli of 24Fun, whose header comment survives in copies all over the web, 1999.
MDN Wayback: 24fun

<script language="JavaScript">
var FMSG = "xbawx.com ";
var chars = [];
for (var i = 0; i < FMSG.length; i++) {
  var c = document.createElement("div");
  c.innerHTML = FMSG.charAt(i);
  c.style.cssText = "position:fixed;left:0;top:0;pointer-events:none;z-index:9999;"
    + "font:bold 14px Verdana;color:#FF00AA";
  document.body.appendChild(c);
  chars.push({ el: c, x: 0, y: 0 });
}
var cx = 0, cy = 0, step = 0;
document.onmousemove = function (e) { cx = e.clientX; cy = e.clientY; };
setInterval(function () {
  step += 0.28;
  for (var i = 0; i < chars.length; i++) {
    var a = step - i * 0.45;
    var tx = cx + Math.cos(a) * (24 + i * 3);
    var ty = cy + Math.sin(a) * (24 + i * 3);
    chars[i].x += (tx - chars[i].x) * 0.4;
    chars[i].y += (ty - chars[i].y) * 0.4;
    chars[i].el.style.left = chars[i].x + "px";
    chars[i].el.style.top = chars[i].y + "px";
  }
}, 30);
</script>
sandboxed demo · breaks nothing but itselfrestart

Custom Cursor

mouse and cursor 1999 still works #

Swaps the mouse pointer for your own .cur or .png file.

In 2026: Works, with two rules modern browsers added: the image must be 128x128 or smaller, and you must supply a keyword fallback after the url().

Where it came from: Microsoft Internet Explorer 4 CSS extensions, 1997. Standardised in CSS 2.1.
MDN CSS 2.1

<style>
body        { cursor: url(https://xbawx.com/inc/img/demo/cursors/hand.cur), auto; }
a:hover     { cursor: url(https://xbawx.com/inc/img/demo/cursors/point.cur), pointer; }
.wait-thing { cursor: url(https://xbawx.com/inc/img/demo/cursors/hourglass.cur), wait; }
</style>

<!-- .ani animated cursors were IE-only and never worked anywhere else.
     A .png works everywhere, and you can set the hotspot: -->
<style>
.custom { cursor: url(https://xbawx.com/inc/img/demo/cursors/star.png) 8 8, crosshair; }
</style>

<!-- The star is a real file in this page, so this one works right now.
     Two rules browsers added since: the image must be 128x128 or smaller,
     and a keyword fallback after the url() is required. -->
<style>
.tryme {
  cursor: url(https://xbawx.com/inc/img/demo/cursors/star.png) 8 8, crosshair;
  display: inline-block; padding: 24px 32px; border: 2px dashed #999;
  font: 13px Verdana;
}
.tryme2 {
  cursor: url(https://xbawx.com/inc/img/demo/cursors/hourglass.cur), wait;
  display: inline-block; padding: 24px 32px; border: 2px dashed #999;
  font: 13px Verdana;
}
</style>
<span class="tryme">point at this box</span>
<span class="tryme2">and this one</span>
sandboxed demo · breaks nothing but itselfrestart

Sparkle On Click

mouse and cursor 2000 still works #

Throws a burst of sparks wherever you click.

In 2026: Works unchanged.

Where it came from: A later cousin of the mouse trail scripts, roughly 2000.
MDN

<p style="font:13px Verdana">Click anywhere in this box.</p>

<script language="JavaScript">
document.onclick = function (e) {
  for (var i = 0; i < 12; i++) {
    (function (n) {
      var s = document.createElement("div");
      s.innerHTML = "\u2734";
      s.style.cssText = "position:fixed;pointer-events:none;z-index:9999;"
        + "font-size:12px;color:hsl(" + (n * 30) + ",100%,60%);"
        + "left:" + e.clientX + "px;top:" + e.clientY + "px;"
        + "transition:all .6s ease-out";
      document.body.appendChild(s);
      setTimeout(function () {
        s.style.left = (e.clientX + Math.cos(n) * 60) + "px";
        s.style.top  = (e.clientY + Math.sin(n) * 60) + "px";
        s.style.opacity = "0";
      }, 10);
      setTimeout(function () { s.parentNode.removeChild(s); }, 700);
    })(i);
  }
};
</script>
sandboxed demo · breaks nothing but itselfrestart

Falling Snow

page decoration 1997 still works #

Snowflakes drift down the page. Switched on every December from 1997 onward.

In 2026: Rewritten from the <layer> / document.all original to plain divs. Same effect, runs everywhere.

Where it came from: Altan of kurt.se wrote the most-copied version, 1997. Reprinted on Dynamic Drive and most other archives.
Wikipedia Wayback: Dynamic Drive snow

<script language="JavaScript">
var FLAKES = 30, flakes = [];
for (var i = 0; i < FLAKES; i++) {
  var f = document.createElement("div");
  f.innerHTML = "\u2744";
  f.style.cssText = "position:fixed;top:-20px;pointer-events:none;z-index:9998;"
    + "color:#fff;text-shadow:0 0 3px #6cf";
  document.body.appendChild(f);
  flakes.push({
    el: f,
    x: Math.random() * window.innerWidth,
    y: Math.random() * window.innerHeight,
    s: 0.6 + Math.random() * 1.6,
    r: 0.4 + Math.random() * 1.2,
    a: Math.random() * 6.28
  });
  f.style.fontSize = (8 + Math.random() * 14) + "px";
}
setInterval(function () {
  for (var i = 0; i < flakes.length; i++) {
    var f = flakes[i];
    f.y += f.s;
    f.a += 0.03;
    if (f.y > window.innerHeight) { f.y = -20; f.x = Math.random() * window.innerWidth; }
    f.el.style.left = (f.x + Math.sin(f.a) * 18) + "px";
    f.el.style.top = f.y + "px";
  }
}, 33);
</script>
sandboxed demo · breaks nothing but itselfrestart

Falling Hearts

page decoration 1999 still works #

The snow script with the flake swapped for a heart. Valentine's Day, every year.

In 2026: Works. Change the character on line 3 for stars, shamrocks, pumpkins or bats.

Where it came from: The falling snow script with the glyph swapped, retagged for February. Free script archives, 1999.
Wikipedia

<script language="JavaScript">
var GLYPH = "\u2665";      // \u2605 star  \u2618 shamrock  \u{1F383} pumpkin
var COLOR = "#FF3366";
var bits = [];
for (var i = 0; i < 24; i++) {
  var b = document.createElement("div");
  b.innerHTML = GLYPH;
  b.style.cssText = "position:fixed;pointer-events:none;z-index:9998;color:" + COLOR
    + ";font-size:" + (10 + Math.random() * 16) + "px";
  document.body.appendChild(b);
  bits.push({ el: b, x: Math.random() * window.innerWidth,
              y: Math.random() * -600, s: 1 + Math.random() * 2,
              a: Math.random() * 6.28 });
}
setInterval(function () {
  for (var i = 0; i < bits.length; i++) {
    var b = bits[i];
    b.y += b.s; b.a += 0.05;
    if (b.y > window.innerHeight) { b.y = -30; b.x = Math.random() * window.innerWidth; }
    b.el.style.left = (b.x + Math.sin(b.a) * 26) + "px";
    b.el.style.top = b.y + "px";
    b.el.style.transform = "rotate(" + (b.a * 20) + "deg)";
  }
}, 33);
</script>
sandboxed demo · breaks nothing but itselfrestart

Background Colour Fader

page decoration 1997 still works #

Fades the page background slowly through the colour wheel.

In 2026: Works unchanged. The original stepped through a hard-coded list of 216 web-safe hex values because HSL did not exist yet.

Where it came from: Free script archives, late 90s. Origin unclear.
MDN

<script language="JavaScript">
var bgHue = 0;
setInterval(function () {
  bgHue = (bgHue + 1) % 360;
  document.body.style.backgroundColor = "hsl(" + bgHue + ",70%,18%)";
}, 80);
</script>
sandboxed demo · breaks nothing but itselfrestart

Starfield

page decoration 1998 still works #

Stars streaming past the screen, like the Windows screensaver.

In 2026: Works. The 1998 version drew this with 60 stacked <layer> tags. This one uses a canvas and is about a thousand times cheaper.

Where it came from: Ported by many people from the Windows 3.1 Starfield Simulation screensaver, which everybody had already seen.
Wikipedia

<style>
  /* Black page. The canvas sits fixed behind everything and the content rides
     on top with a higher z-index. A z-index:-1 canvas would vanish behind an
     opaque body background, which is the classic way this effect "breaks". */
  html, body { margin: 0; height: 100%; background: #000; overflow: hidden; }
  #stars { position: fixed; inset: 0; z-index: 0; display: block; }
  .cyber { position: relative; z-index: 1; margin: 0 0 8px; padding: 0 18px;
           color: #fff; font-family: Verdana, sans-serif; text-shadow: 0 0 6px #39f; }
  h2.cyber { padding-top: 16px; }
</style>

<canvas id="stars"></canvas>
<h2 class="cyber">WELCOME TO CYBERSPACE</h2>
<p class="cyber">Stars stream past behind the text, like the Windows 3.1 screensaver.</p>

<script language="JavaScript">
var cv = document.getElementById("stars"), cx = cv.getContext("2d"), st = [];
function sizeIt() { cv.width = window.innerWidth; cv.height = window.innerHeight; }
sizeIt(); window.onresize = sizeIt;
for (var i = 0; i < 220; i++) {
  st.push({ x: (Math.random() - 0.5) * 2, y: (Math.random() - 0.5) * 2,
            z: Math.random() });
}
setInterval(function () {
  cx.fillStyle = "#000"; cx.fillRect(0, 0, cv.width, cv.height);
  var hw = cv.width / 2, hh = cv.height / 2;
  for (var i = 0; i < st.length; i++) {
    var s = st[i];
    s.z -= 0.006;
    if (s.z <= 0.01) { s.z = 1; s.x = (Math.random() - 0.5) * 2; s.y = (Math.random() - 0.5) * 2; }
    var px = hw + (s.x / s.z) * hw, py = hh + (s.y / s.z) * hh;
    var r = (1 - s.z) * 2.4;
    cx.fillStyle = "rgba(255,255,255," + (1 - s.z) + ")";
    cx.fillRect(px, py, r, r);
  }
}, 33);
</script>
sandboxed demo · breaks nothing but itselfrestart

Tiled Background

page decoration 1995 still works #

A small image repeated behind the whole page, cut so its edges join invisibly.

In 2026: Works, but do not use the <body background=> attribute any more, it is obsolete. The CSS version below is identical and lets you fix the tile while the page scrolls.

Where it came from: Netscape Navigator 1.1 BODY BACKGROUND attribute, 1995.
MDN HTML standard

<!-- The 1995 way (obsolete attribute, still renders) -->
<body bgcolor="#000000" text="#00FF00"
      background="https://xbawx.com/btn/assets/e9/e9f7b1b0191adffe6cf00cf4516c9eaab7ebeac1.gif">

<!-- The way to do it now -->
<style>
body {
  background-image: url(https://xbawx.com/btn/assets/e9/e9f7b1b0191adffe6cf00cf4516c9eaab7ebeac1.gif);
  background-repeat: repeat;
  background-color: #000;       /* shows while the tile loads */
  background-attachment: fixed; /* tile stays put while you scroll */
  color: #0f0;
}
</style>

<font face="Verdana" size="2">
  Tiled backgrounds live in the xbawx button gallery under
  <b>textures</b>: /btn/?collection=textures
</font>
sandboxed demo · breaks nothing but itselfrestart

Disco Flash

page decoration 1997 still works #

Flashes the background between two colours. Put this on your entry page. Please do not.

In 2026: Works. Genuinely a seizure risk above about 3 flashes per second, so this version is throttled and stops after 12 flashes.

Where it came from: Origin unclear, and probably better left that way.
Wikipedia

<script language="JavaScript">
var on = false, flashes = 0;
var disco = setInterval(function () {
  on = !on;
  document.body.style.backgroundColor = on ? "#FF00FF" : "#00FFFF";
  if (++flashes > 12) {
    clearInterval(disco);
    document.body.style.backgroundColor = "";
  }
}, 350);
</script>
sandboxed demo · breaks nothing but itselfrestart

Jump Menu

navigation 1996 still works #

A dropdown that takes you straight to the page you pick.

In 2026: Works. Keep a Go button next to it for keyboard users, who otherwise trigger a navigation by arrowing through the list.

Where it came from: Netscape JavaScript examples, 1996. Later shipped as a built-in Dreamweaver behavior.
MDN

<form name="jumper">
  <select name="dest" onchange="if(this.value) location.href=this.value">
    <option value="">-- pick a page --</option>
    <option value="/">Home</option>
    <option value="/about.html">About Me</option>
    <option value="/links.html">My Links</option>
    <option value="/guestbook.html">Sign My Guestbook</option>
  </select>
  <input type="button" value="Go"
         onclick="if(document.jumper.dest.value) location.href=document.jumper.dest.value">
</form>
sandboxed demo · breaks nothing but itselfrestart

Back And Forward Buttons

navigation 1996 still works #

Browser back and forward, as buttons on the page.

In 2026: Works unchanged.

Where it came from: Netscape JavaScript 1.0 history object, 1996.
MDN

<a href="javascript:history.back()">&lt;&lt; Back</a> |
<a href="javascript:history.forward()">Forward &gt;&gt;</a> |
<a href="javascript:location.reload()">Reload</a>

<!-- Or as real buttons -->
<button onclick="history.back()">&laquo; Back</button>
<button onclick="history.forward()">Forward &raquo;</button>
sandboxed demo · breaks nothing but itselfrestart

Webring Navigation Bar

navigation 1997 still works #

The prev / random / next strip that linked thousands of hobby sites together.

In 2026: The markup works fine. The ring servers it pointed at (WebRing, RingSurf) are mostly gone, so you now host the ring yourself.

Where it came from: WebRing, started by Sage Weil in 1995. RingSurf and the other ring hosts copied the fragment shape.
Wikipedia

<table border="2" cellpadding="6" bgcolor="#000040" align="center">
<tr><td align="center">
  <font face="Verdana" size="2" color="#FFFFFF">
    This site is a member of the<br>
    <b><font color="#FFFF00">Retro Web Ring</font></b>
  </font><br><br>
  <a href="/ring/prev?id=42">[ Previous ]</a>
  <a href="/ring/random">[ Random ]</a>
  <a href="/ring/next?id=42">[ Next ]</a>
  <a href="/ring/list">[ List Sites ]</a>
  <br><br>
  <font face="Verdana" size="1" color="#AAAAAA">
    Want to join? <a href="/ring/join">Apply here</a>
  </font>
</td></tr>
</table>
sandboxed demo · breaks nothing but itselfrestart

Image Rollover

navigation 1996 still works #

Swaps a button image when the mouse moves over it. The definitive 90s script.

In 2026: Works. Preloading the hover image is the whole point: without it the button flickers blank the first time you hover, which is exactly what happened in 1996.

Where it came from: Netscape JavaScript 1.1 image object, 1996. Probably the single most copied script of the decade.
MDN MDN Image()

<script language="JavaScript">
<!--
// Preload so there is no flicker on first hover
var onImg = new Image();
onImg.src  = "https://xbawx.com/btn/assets/a6/a6123e8f5e63e584a09d69d3555770b2295cf761.gif";
var offImg = new Image();
offImg.src = "https://xbawx.com/btn/assets/f8/f8fe94a7667d1041aee087e1c130f93c0b11462e.gif";

function swap(name, src) { document.images[name].src = src; }
//-->
</script>

<a href="/"
   onmouseover="swap('home', onImg.src)"
   onmouseout="swap('home', offImg.src)">
  <img name="home" src="https://xbawx.com/btn/assets/f8/f8fe94a7667d1041aee087e1c130f93c0b11462e.gif"
       width="88" height="31" border="0" alt="Home">
</a>
sandboxed demo · breaks nothing but itselfrestart

Break Out Of Frames

navigation 1997 still works #

Forces your page to the top window if somebody loads it inside their frameset.

In 2026: Still works and is still the right idea. The modern equivalent is the X-Frame-Options or frame-ancestors header, which a script cannot be talked out of.

Where it came from: Circulated widely from about 1997, once people started framing other people's sites.
MDN MDN frame-ancestors

<script language="JavaScript">
if (top.location != self.location) {
  top.location = self.location.href;
}
</script>

<p style="font:13px Verdana">This demo just tried to bust out of its frame. The
sandbox around it refused the navigation, which is the modern header doing for
every site what this script did for one page.</p>

<!-- The 2026 version. Send this as an HTTP response header instead: -->
<!-- Content-Security-Policy: frame-ancestors 'self' -->
sandboxed demo · breaks nothing but itselfrestart

Expanding Menu

navigation 1999 still works #

A folder-style menu whose sections open and close when you click the heading.

In 2026: Works. In 1999 this needed three code paths: document.all for IE, document.layers for Netscape 4, and getElementById for everything after.

Where it came from: Dynamic Drive, roughly 1999, originally shipped in three browser-specific versions.
Wikipedia Wayback: Dynamic Drive

<style>
.menu-head { cursor: pointer; font: bold 13px Verdana; color: #FFCC00;
             background: #000060; padding: 4px 8px; margin-top: 2px; }
.menu-body { display: none; background: #000030; padding: 6px 18px; }
.menu-body a { color: #99CCFF; display: block; padding: 2px 0; }
</style>

<div class="menu-head" onclick="toggleMenu('m1')">+ My Pages</div>
<div class="menu-body" id="m1">
  <a href="/about.html">About Me</a>
  <a href="/pets.html">My Pets</a>
</div>

<div class="menu-head" onclick="toggleMenu('m2')">+ Links</div>
<div class="menu-body" id="m2">
  <a href="/links.html">Cool Sites</a>
  <a href="/ring.html">Web Ring</a>
</div>

<script language="JavaScript">
function toggleMenu(id) {
  var b = document.getElementById(id);
  var open = b.style.display === "block";
  b.style.display = open ? "none" : "block";
  b.previousSibling.previousSibling.innerHTML =
    (open ? "+ " : "- ") + b.previousSibling.previousSibling.innerHTML.substring(2);
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Digital Clock

time and date 1996 still works #

A live ticking clock on the page.

In 2026: Works unchanged.

Where it came from: Netscape JavaScript examples, 1996.
MDN

<div id="clock" style="font:bold 20px 'Courier New';color:#00FF00;
     background:#000;display:inline-block;padding:6px 12px;border:2px inset #333">
</div>

<script language="JavaScript">
function tick() {
  var d = new Date();
  var h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
  var ap = h >= 12 ? "PM" : "AM";
  h = h % 12; if (h === 0) h = 12;
  document.getElementById("clock").innerHTML =
    h + ":" + (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s + " " + ap;
  setTimeout(tick, 1000);
}
tick();
</script>
sandboxed demo · breaks nothing but itselfrestart

Last Updated Stamp

time and date 1995 still works #

Prints the file's own last-modified date. No maintenance, ever.

In 2026: Works, with one caveat: if the server sends no Last-Modified header the browser substitutes the current time, so the page claims it was updated just now.

Where it came from: Netscape JavaScript 1.0 document object, 1996.
MDN

<script language="JavaScript">
document.write("This page last updated: " + document.lastModified);
</script>

<!-- Nicer formatting -->
<script language="JavaScript">
var lm = new Date(document.lastModified);
var months = ["January","February","March","April","May","June",
              "July","August","September","October","November","December"];
document.write("Last updated " + months[lm.getMonth()] + " "
             + lm.getDate() + ", " + lm.getFullYear());
</script>
sandboxed demo · breaks nothing but itselfrestart

Countdown Clock

time and date 1998 still works #

Counts down to a date. Half the web pointed this at midnight, 31 December 1999.

In 2026: Works unchanged. The original read new Date("January 1, 2000 00:00:00"), which is long past, so this page rewrites the target to five minutes from your page load and the demo really counts. Set TARGET to any date you like. Keep the Z on the end, or the browser reads the date as local time and everyone in a different timezone gets a different answer.

Where it came from: Free script archives. Usage spiked sharply through 1999 for obvious reasons.
MDN

<div id="cd" style="font:bold 16px Verdana;color:#FF0000"></div>

<script language="JavaScript">
var TARGET = new Date("2026-08-21T18:06:47Z");
function countdown() {
  var left = TARGET - new Date();
  if (left <= 0) {
    document.getElementById("cd").innerHTML = "IT'S HERE!!!";
    return;
  }
  var d = Math.floor(left / 86400000);
  var h = Math.floor(left / 3600000) % 24;
  var m = Math.floor(left / 60000) % 60;
  var s = Math.floor(left / 1000) % 60;
  document.getElementById("cd").innerHTML =
    d + " days, " + h + " hours, " + m + " minutes, " + s + " seconds";
  setTimeout(countdown, 1000);
}
countdown();
</script>
sandboxed demo · breaks nothing but itselfrestart

Greeting By Time Of Day

time and date 1997 still works #

Says good morning, good afternoon or good evening depending on the visitor's clock.

In 2026: Works unchanged.

Where it came from: Netscape JavaScript examples, 1996.
MDN

<script language="JavaScript">
var h = new Date().getHours();
var greet;
if (h < 5)       greet = "You're up late";
else if (h < 12) greet = "Good morning";
else if (h < 18) greet = "Good afternoon";
else             greet = "Good evening";
document.write("<h2>" + greet + ", and welcome to my page!</h2>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Days Online Counter

time and date 1998 still works #

Prints how long the site has been up, counting from the day you launched it.

In 2026: Works unchanged.

Where it came from: Free script archives, late 90s.
MDN

<script language="JavaScript">
var LAUNCH = new Date("June 14, 1997");
var days = Math.floor((new Date() - LAUNCH) / 86400000);
document.write("This site has been online for <b>" + days + "</b> days.");
</script>
sandboxed demo · breaks nothing but itselfrestart

Random Image

images and media 1996 still works #

Shows a different picture from your list every time the page loads.

In 2026: Works unchanged.

Where it came from: Netscape JavaScript examples, 1996.
MDN

<script language="JavaScript">
var pics = [
  "https://xbawx.com/btn/assets/7c/7cada91ec2730b0fb58b60bab3f16e67985dd27b.gif",
  "https://xbawx.com/btn/assets/cd/cd5e5da0a6c7a9bb95bb3ce5a919aea7c39f50b0.gif",
  "https://xbawx.com/btn/assets/d7/d7e43e74e65cea13f9566328dc82277862763030.gif",
  "https://xbawx.com/btn/assets/d2/d2985e48c7ac7bdbd512da8ea61ab68e76c1dd83.gif"
];
var pick = pics[Math.floor(Math.random() * pics.length)];
document.write('<img src="' + pick + '" border="2" alt="Random picture">');
</script>
sandboxed demo · breaks nothing but itselfrestart

Banner Rotator

images and media 1997 still works #

Cycles through a set of 468x60 banners on a timer, each one clickable.

In 2026: Works unchanged.

Where it came from: Standard fare from the banner exchange networks, LinkExchange and its imitators, 1997.
Wikipedia

<a href="#" id="bannerLink" target="_blank">
  <img id="banner" width="468" height="60" border="0" alt=""
       src="https://xbawx.com/btn/assets/1a/1a8abd6c5408549cb231cd1123976256dc0e4564.gif">
</a>

<script language="JavaScript">
var banners = [
  { img: "https://xbawx.com/btn/assets/1a/1a8abd6c5408549cb231cd1123976256dc0e4564.gif",
    url: "http://www.example.com/" },
  { img: "https://xbawx.com/btn/assets/c7/c704f793437b37858d0104ef6e9b214caf95ab24.gif",
    url: "http://www.example.net/" },
  { img: "https://xbawx.com/btn/assets/7b/7bf9dba51af5b0a3b14b3c15f152d0cb862fe23f.gif",
    url: "http://www.example.org/" }
];
var bi = 0;
setInterval(function () {
  bi = (bi + 1) % banners.length;
  document.getElementById("banner").src = banners[bi].img;
  document.getElementById("bannerLink").href = banners[bi].url;
}, 3000);
</script>
sandboxed demo · breaks nothing but itselfrestart

Image Slideshow

images and media 1997 still works #

Next and previous buttons stepping through a set of photos.

In 2026: Works. Preloading matters here for the same reason it does on rollovers.

Where it came from: Free script archives, roughly 1998. Later a Dreamweaver behavior.
MDN

<img id="slide" border="3"
     src="https://xbawx.com/btn/assets/7c/7cada91ec2730b0fb58b60bab3f16e67985dd27b.gif"><br>
<button onclick="step(-1)">&laquo; Previous</button>
<span id="slideNum">1 of 4</span>
<button onclick="step(1)">Next &raquo;</button>

<script language="JavaScript">
var slides = [
  "https://xbawx.com/btn/assets/7c/7cada91ec2730b0fb58b60bab3f16e67985dd27b.gif",
  "https://xbawx.com/btn/assets/cd/cd5e5da0a6c7a9bb95bb3ce5a919aea7c39f50b0.gif",
  "https://xbawx.com/btn/assets/d7/d7e43e74e65cea13f9566328dc82277862763030.gif",
  "https://xbawx.com/btn/assets/d3/d37aa48fdebf21f4f35faabc4e983573b2553fbe.gif"
];
var si = 0;
for (var i = 0; i < slides.length; i++) { var p = new Image(); p.src = slides[i]; }
function step(n) {
  si = (si + n + slides.length) % slides.length;
  document.getElementById("slide").src = slides[si];
  document.getElementById("slideNum").innerHTML = (si + 1) + " of " + slides.length;
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Background MIDI Music

images and media 1996 dead #

Auto-playing background music, the sound of the 1996 web.

In 2026: Both original tags are dead: <bgsound> was IE-only and <embed> for MIDI needed a plugin nobody ships. Browsers also block autoplay with sound outright now. The working replacement is an <audio> tag the visitor presses play on. Pick a MIDI from the xbawx sound archive at /snd/ and use its preview mp3, which is the one wired up below (Scott Joplin, public domain).

Where it came from: Microsoft Internet Explorer 2 BGSOUND, 1996, alongside Netscape LiveAudio the same year.
MDN xbawx sound archive

<!-- Internet Explorer -->
<bgsound src="music/theme.mid" loop="infinite">

<!-- Netscape (needed the LiveAudio plugin) -->
<embed src="music/theme.mid" autostart="true" loop="true"
       hidden="true" width="2" height="2">

<!-- Both at once, which is what everybody actually shipped -->
<object data="music/theme.mid" type="audio/midi">
  <embed src="music/theme.mid" autostart="true" loop="true" hidden="true">
</object>

<!-- The 2026 version. Autoplay with sound is blocked, so give them a button.
     Every track at https://xbawx.com/snd/ has a rendered mp3 at the same
     hash under /snd/preview/, and the original .mid next to it. -->
<audio id="bgm" loop
  src="https://xbawx.com/snd/preview/70/70db912c2b37cc4682e540b712576f1338089503.mp3">
</audio>
<button onclick="document.getElementById('bgm').play()">Play music</button>
<button onclick="document.getElementById('bgm').pause()">Stop</button>
sandboxed demo · breaks nothing but itselfrestart

Sound On Mouseover

images and media 1998 partly #

Plays a click or beep when the pointer crosses a menu item.

In 2026: Partly works. Browsers block audio until the visitor has interacted with the page, so the first hover is silent and every hover after a click is fine.

Where it came from: Free script archives, roughly 1998. Needed the LiveAudio plugin to work in Netscape.
MDN

<audio id="sfx" src="https://xbawx.com/snd/assets/59/59053cb6dbb43174e264d577ae4d386a8e94f762.wav"
       preload="auto"></audio>

<a href="/" onmouseover="playSfx()">Home</a>
<a href="/links.html" onmouseover="playSfx()">Links</a>

<script language="JavaScript">
function playSfx() {
  var a = document.getElementById("sfx");
  a.currentTime = 0;
  var p = a.play();
  if (p && p.catch) p.catch(function () {});   // blocked until first click
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Popup Window

windows and alerts 1996 partly #

Opens a small chromeless window sized to your content.

In 2026: Works only when it happens inside a click. A window.open on page load is blocked by every browser, which is the whole reason popup blockers exist.

Where it came from: Netscape JavaScript 1.0 window.open, 1996.
MDN

<script language="JavaScript">
function popUp(url, w, h) {
  window.open(url, "popup",
    "width=" + w + ",height=" + h +
    ",scrollbars=yes,resizable=yes,toolbar=no,menubar=no,status=no");
}
</script>

<a href="javascript:popUp('/photo.html', 420, 340)">View the photo</a>

<!-- Straight onto a link, with a no-JS fallback -->
<a href="/photo.html" target="_blank"
   onclick="popUp(this.href, 420, 340); return false">View the photo</a>
sandboxed demo · breaks nothing but itselfrestart

Welcome Alert

windows and alerts 1996 still works #

A dialog box that greets the visitor the moment the page loads.

In 2026: Works. It was considered friendly in 1996 and it is considered hostile now.

Where it came from: Netscape JavaScript 1.0, 1996. It was considered friendly at the time.
MDN

<script language="JavaScript">
alert("Welcome to my home page!\n\nPlease sign my guestbook before you leave!");
</script>

<!-- Or only on the visitor's first arrival, using a cookie -->
<script language="JavaScript">
try {
  if (document.cookie.indexOf("seenwelcome=1") === -1) {
    alert("Welcome to my home page!");
    document.cookie = "seenwelcome=1; path=/; max-age=31536000";
  }
} catch (e) { /* a sandboxed frame has no cookie access */ }
</script>
sandboxed demo · breaks nothing but itselfrestart

Confirm Before Leaving

windows and alerts 1999 partly #

Asks Are you sure you want to leave? when the visitor navigates away.

In 2026: Partly works. Browsers still fire the event but they ignore your message and show their own generic wording, and they refuse to prompt at all unless the visitor has interacted with the page.

Where it came from: Microsoft Internet Explorer 4 onbeforeunload, 1997. Standardised much later.
MDN

<script language="JavaScript">
window.onbeforeunload = function (e) {
  var msg = "Are you sure you want to leave? Don't forget the guestbook!";
  (e || window.event).returnValue = msg;   // browsers ignore the text now
  return msg;
};
</script>

<p style="font:13px Verdana">The handler is armed. On a real page, leaving after
typing or clicking would now show the browser's own generic prompt. Nothing in
this frame ever navigates, so there is nothing to see here.</p>
sandboxed demo · breaks nothing but itselfrestart

Ask For Their Name

windows and alerts 1996 still works #

Prompts for a name on arrival and greets them by it.

In 2026: Works. Storing the answer in a cookie so they are only asked once is the part most 1996 copies of this script forgot.

Where it came from: Netscape JavaScript 1.0 prompt, 1996.
MDN

<script language="JavaScript">
var who = null, m = null;
try { m = document.cookie.match(/visitorname=([^;]+)/); } catch (e) { m = null; }
if (m) {
  who = decodeURIComponent(m[1]);
} else {
  who = prompt("Hi! What's your name?", "");
  try {
    if (who) document.cookie = "visitorname=" + encodeURIComponent(who)
                             + "; path=/; max-age=31536000";
  } catch (e) {}
}
if (who) document.write("<h2>Welcome, " + who + "!</h2>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Browser Detection

browser tricks 1997 partly #

Works out which browser the visitor has, so you can serve them the right page.

In 2026: The properties still exist but every browser now lies in its user agent string precisely because of scripts like this. Test for the feature you need instead.

Where it came from: Netscape JavaScript navigator object, 1996. This script is the reason every user agent string now lies about itself.
MDN MDN UA sniffing

<script language="JavaScript">
var isNS = (navigator.appName === "Netscape");
var isIE = (navigator.appName.indexOf("Microsoft") !== -1);
var ver  = parseInt(navigator.appVersion, 10);

document.write("You are using " + navigator.appName
             + " version " + navigator.appVersion + "<br>");

if (isIE && ver >= 4)      document.write("Loading the IE4 version...");
else if (isNS && ver >= 4) document.write("Loading the Netscape 4 version...");
else                       document.write("Loading the plain HTML version...");
</script>

<!-- What to do in 2026: test the capability, not the name -->
<script>
if (window.IntersectionObserver) { /* modern path */ }
else { /* fallback path */ }
</script>
sandboxed demo · breaks nothing but itselfrestart

Best Viewed At

browser tricks 1997 still works #

Reads the visitor's screen size and tells them off if it is not 800x600.

In 2026: Works, but screen.width was always the whole display, never the browser window, and on a HiDPI screen it now reports CSS pixels rather than hardware pixels. Use window.innerWidth if you want the size of the actual viewport.

Where it came from: Netscape JavaScript 1.2 screen object, 1997.
MDN

<script language="JavaScript">
document.write("Your screen is " + screen.width + "x" + screen.height
             + " at " + screen.colorDepth + " bit colour.<br>");

if (screen.width < 800) {
  document.write("<b>This site is best viewed at 800x600 or higher!</b>");
}
</script>

<!-- The size that actually matters is the window, not the screen.
     Read it after load: during parsing this frame has no size yet, so a
     document.write here would report 0x0. -->
<p id="vp" style="font:13px Verdana"></p>
<script>
window.onload = function () {
  document.getElementById("vp").innerHTML = "Your browser window is "
    + window.innerWidth + "x" + window.innerHeight;
};
</script>
sandboxed demo · breaks nothing but itselfrestart

Where Did You Come From

browser tricks 1997 still works #

Prints the page the visitor clicked through from.

In 2026: Works, but it is empty far more often than it used to be. By default a browser now sends only the origin and not the full path on a cross-origin click, and nothing at all when an HTTPS page links to an HTTP one.

Where it came from: Netscape JavaScript 1.0 document.referrer, 1996.
MDN MDN Referrer-Policy

<script language="JavaScript">
if (document.referrer && document.referrer.length) {
  document.write("Thanks for coming over from <b>"
               + document.referrer + "</b>!");
} else {
  document.write("Welcome! Did you bookmark me? :)");
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Bookmark This Page

browser tricks 1998 dead #

A link that adds your site to the visitor's favourites.

In 2026: Dead. window.external.AddFavorite was Internet Explorer only and was removed. No browser lets a page write a bookmark any more. The best you can do is tell them the keyboard shortcut.

Where it came from: Microsoft Internet Explorer 4 external object, MSDN, 1997.
MDN

<script language="JavaScript">
function addBookmark() {
  if (window.external && window.external.AddFavorite) {
    window.external.AddFavorite(location.href, document.title);   // IE only
  } else if (window.sidebar && window.sidebar.addPanel) {
    window.sidebar.addPanel(document.title, location.href, "");   // Netscape
  } else {
    alert("Press " + (navigator.platform.indexOf("Mac") > -1 ? "Cmd" : "Ctrl")
        + "+D to bookmark this page.");
  }
}
</script>

<a href="javascript:addBookmark()">Bookmark this page!</a>
sandboxed demo · breaks nothing but itselfrestart

Set As Homepage

browser tricks 1999 dead #

Makes your site the visitor's browser home page in one click.

In 2026: Dead, and good. It was an IE-only behavior: property, always gated behind a confirm dialog, and it was abused constantly. It died with IE's legacy document modes, and no other browser ever implemented it.

Where it came from: Microsoft Internet Explorer 5 default behaviors, MSDN, 1999.
Wikipedia

<a href="#" onclick="this.style.behavior='url(#default#homepage)';
                     this.setHomePage('http://www.example.com/'); return false">
  Make this your home page!
</a>

<!-- Nothing replaced it. If you want to be somebody's home page, be good. -->
sandboxed demo · breaks nothing but itselfrestart

Print This Page

browser tricks 1997 still works #

Opens the print dialog.

In 2026: Works unchanged. Pair it with a print stylesheet so the printout is not your navigation and a tiled background.

Where it came from: Netscape Navigator 4 window.print, 1997. Internet Explorer followed in 5.
MDN

<a href="javascript:window.print()">
  <img src="https://xbawx.com/inc/img/demo/printer.gif" border="0" alt=""> Print this page
</a>

<!-- Worth adding: -->
<style media="print">
  .nav, .banner, .guestbook { display: none; }
  body { background: #fff; color: #000; }
</style>
sandboxed demo · breaks nothing but itselfrestart

Disable Right Click

browser tricks 1998 partly #

Blocks the context menu so nobody can steal your images. Allegedly.

In 2026: The menu really is suppressed, and it stops nobody: the images are already on their machine. It mostly annoys people who wanted to open a link in a new tab.

Where it came from: Free script archives from about 1998. Many versions, all equally ineffective.
MDN

<script language="JavaScript">
function noRightClick() {
  alert("Sorry, right clicking is disabled! (c) my site");
  return false;
}
document.oncontextmenu = noRightClick;
document.onmousedown = function (e) {
  if ((e || window.event).button === 2) return noRightClick();
};
</script>

<p style="font:13px Verdana">Right-click anywhere in this box.</p>

<!-- The whole page, the old way -->
<body oncontextmenu="return false" ondragstart="return false"
      onselectstart="return false">
sandboxed demo · breaks nothing but itselfrestart

Hide Your Email From Spambots

browser tricks 1998 still works #

Builds a mailto link in JavaScript so a plain-text harvester never sees it.

In 2026: Works, and still helps against the simplest scrapers. Anything that runs JavaScript reads it fine, so treat it as speed bump, not armour.

Where it came from: Anti-spam advice from Usenet and the web FAQs of the late 90s.
Wikipedia

<script language="JavaScript">
var user = "webmaster";
var host = "example.com";
document.write('<a href="mail' + 'to:' + user + '@' + host + '">'
             + user + ' at ' + host + '</a>');
</script>

<noscript>webmaster at example dot com</noscript>

<!-- The character-code version, for maximum 1998 authenticity -->
<script language="JavaScript">
var e = "";
var c = [119,101,98,64,101,120,97,109,112,108,101,46,99,111,109];
for (var i = 0; i < c.length; i++) e += String.fromCharCode(c[i]);
document.write('<a href="mailto:' + e + '">Email me</a>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Hosted Hit Counter

counters and guestbook 1996 still works #

One image tag that counts every load of your page. Server side, so it counts everyone.

In 2026: Works. This is the pattern every free counter service used, and it is what the xbawx counters at /counter/ serve. Make your own there and paste the tag it gives you. The title attribute is what shows on hover and the alt is what a screen reader reads, which between them carry the attribution the free services all wanted in exchange.

Where it came from: The pattern used by Web-Counter, Digits and every other free counter service from 1995 onward.
Wikipedia Make one on xbawx

<!-- Make one at https://xbawx.com/counter/ and paste the tag it gives you -->
<center>
<font face="Verdana" size="1">You are visitor number</font><br>
<a href="https://xbawx.com/counter/" target="_blank"
   title="Get your own free counter at xbawx.com">
  <img src="https://xbawx.com/counter/3cswt32t59r8.png" border="0"
       alt="Visitor counter" title="Hit counter powered by xbawx.com">
</a><br>
<font face="Verdana" size="1">since June 1997</font>
</center>
sandboxed demo · breaks nothing but itselfrestart

Sign My Guestbook

counters and guestbook 1996 still works #

The sign and view guestbook block that closed out every personal home page.

In 2026: The markup works. The free CGI guestbooks it pointed at are all gone, so the form has to post somewhere you control.

Where it came from: Matt's Script Archive guestbook.pl, 1995, and the free host guestbooks that copied it.
Wikipedia Matt's Script Archive

<center>
<table border="0" cellpadding="8">
<tr>
  <td align="center">
    <a href="/cgi-bin/guestbook.cgi?action=sign">
      <img src="https://xbawx.com/btn/assets/8b/8bdf0973981f85adcb4e414fdd3c29b6b5c09f87.gif"
           border="0" alt="Sign my guestbook"></a>
  </td>
  <td align="center">
    <a href="/cgi-bin/guestbook.cgi?action=view">
      <img src="https://xbawx.com/btn/assets/36/368d7990a73611324b866899545704642fd3869c.gif"
           border="0" alt="View my guestbook"></a>
  </td>
</tr>
</table>
<font face="Comic Sans MS" size="2" color="#FF00FF">
  Please sign my guestbook before you go!!
</font>
</center>
sandboxed demo · breaks nothing but itselfrestart

Under Construction Notice

counters and guestbook 1995 still works #

The apology block. Almost every page on the 90s web had one somewhere.

In 2026: Works. Grab an animated construction GIF from the xbawx button gallery: /btn/?collection=under-construction has about 1,800 of them.

Where it came from: Universal. The animated construction worker is usually traced to a mid-90s clipart set of unclear origin.
Wikipedia xbawx gallery

<center>
<img src="https://xbawx.com/btn/assets/eb/ebe0ed36a9171d5e37895f37e18809cf877f99b0.gif"
     width="301" height="110" alt="Under construction">
<br>
<font face="Comic Sans MS" size="3" color="#FFCC00">
  <b>This page is under construction!</b>
</font>
<br>
<font face="Verdana" size="2">
  Please check back soon. Last updated
  <script language="JavaScript">document.write(document.lastModified);</script>
</font>
<br>
<img src="https://xbawx.com/btn/assets/16/163d8fb055a9e0c37cc968d1bba32b69b72bf351.gif"
     width="64" height="68" alt="Under construction">
</center>
sandboxed demo · breaks nothing but itselfrestart

The SPACER Tag

dead tags 1996 dead #

Netscape's answer to the spacer gif: an element whose only job was to take up room.

In 2026: Dead. Netscape 3 and 4 shipped it, Gecko kept honouring it in quirks mode until Firefox 4 in 2011, and nothing renders it now, so the block below collapses to nothing. Use margin or padding.

Where it came from: Netscape Navigator 3.0 HTML extensions, 1996. Never proposed to the W3C.
MDN HTML standard

<!-- Push down 40 pixels -->
Top of page.<spacer type="vertical" size="40">Forty pixels lower.

<!-- Indent 30 pixels -->
<spacer type="horizontal" size="30">Indented text.

<!-- A rectangular hole in the flow -->
<spacer type="block" width="100" height="60" align="left">
Text wraps around the hole.
sandboxed demo · breaks nothing but itselfrestart

The MULTICOL Tag

dead tags 1996 dead #

Newspaper columns, fifteen years before CSS could do them.

In 2026: Dead everywhere. It was a Netscape extension that shipped in 3.0 and was gone by 6.0. CSS column-count does the same job now and is in every browser.

Where it came from: Netscape Navigator 3.0 HTML extensions, 1996.
MDN

<multicol cols="3" gutter="20" width="600">
  <p>Long article text flows into three columns automatically. This was the
  only way to do it without slicing your copy into table cells by hand.</p>
</multicol>

<!-- The 2026 version -->
<div style="column-count: 3; column-gap: 20px">
  <p>Same thing, in every browser.</p>
</div>
sandboxed demo · breaks nothing but itselfrestart

LAYER and ILAYER

dead tags 1997 dead #

Netscape 4's positioned boxes, and the reason half of 1998's DHTML has two code paths.

In 2026: Dead. Netscape 4 shipped LAYER while Microsoft shipped CSS positioning, so every DHTML script of the period branched on document.layers versus document.all. Netscape 6 dropped LAYER entirely and the branch went with it.

Where it came from: Netscape Communicator 4.0 DHTML reference, 1997.
Wikipedia

<layer id="float1" left="50" top="100" width="200" bgcolor="#FFFFCC"
       visibility="show" z-index="2">
  A positioned box, Netscape 4 style.
</layer>

<!-- Inline version, flows with the text -->
<ilayer width="120" height="60" bgcolor="#CCFFCC">inline layer</ilayer>

<!-- Shown only to browsers that DO NOT understand layers -->
<nolayer>
  <div style="position:absolute; left:50px; top:100px; width:200px;
              background:#FFFFCC">Everyone else got this.</div>
</nolayer>

<script language="JavaScript">
// Moving one, Netscape 4 style
if (document.layers) {
  document.layers["float1"].left = 120;
  document.layers["float1"].visibility = "hide";
}
</script>
sandboxed demo · breaks nothing but itselfrestart

The PLAINTEXT Tag

dead tags 1994 partly #

The one-way door. Everything after it is literal text and there is no way back.

In 2026: Browsers still honour it, which is why the demo below stops rendering HTML halfway through. There is no closing tag: it was defined that way in 1991 and never fixed. It was already deprecated in HTML 2.0.

Where it came from: HTML Tags, Tim Berners-Lee, 1991. Deprecated by HTML 2.0 in 1995.
MDN HTML Tags, 1991

<p>This paragraph renders normally.</p>

<plaintext>
<b>From here down nothing is HTML any more.</b>
<script>alert('this never runs')</script>
There is no closing tag. There never was.
sandboxed demo · breaks nothing but itselfrestart

XMP and LISTING

dead tags 1994 partly #

Preformatted text from before PRE settled the argument. Three tags for one job.

In 2026: Both still render in browsers as a courtesy. HTML 2.0 already deprecated both in favour of PRE, and HTML 3.2 declared them obsolete. LISTING was defined as 132 columns, XMP as 80. Use PRE.

Where it came from: HTML 2.0 (RFC 1866), 1995, which already told authors to use PRE instead. Declared obsolete in HTML 3.2, 1997.
MDN RFC 1866

<xmp>
  <b>Markup inside XMP is shown, not parsed.</b>
  No escaping needed.
</xmp>

<listing>
  Same idea, but the spec said render this at 132 characters wide.
</listing>

<!-- What survived -->
<pre>&lt;b&gt;PRE needs its angle brackets escaped.&lt;/b&gt;</pre>
sandboxed demo · breaks nothing but itselfrestart

The ISINDEX Tag

dead tags 1994 dead #

One tag that produced an entire search box. The web's first form control.

In 2026: Removed from browsers between 2014 and 2017, Chrome first and Firefox last. It predates FORM: the browser drew a text field, and pressing enter re-requested the same URL with the text appended as a query.

Where it came from: HTML Tags, 1991, and HTML 2.0 (RFC 1866), 1995.
Wikipedia WHATWG removal PR HTML Tags, 1991

<!-- In the HEAD, or the BODY, both were allowed -->
<isindex prompt="Search this site: ">

<!-- With its own handler, which came later -->
<isindex prompt="Find: " action="/cgi-bin/search.pl">

<!-- What replaced it -->
<form action="/cgi-bin/search.pl" method="get">
  Search this site: <input type="text" name="q">
</form>
sandboxed demo · breaks nothing but itselfrestart

The COMMENT Tag

dead tags 1996 dead #

Internet Explorer shipped an element that meant the same thing as an SGML comment.

In 2026: Dead, and it never worked outside Internet Explorer, so anything you put in one was visible to half your visitors. That is the whole story of this tag.

Where it came from: Microsoft Internet Explorer 3 HTML reference, 1996.
Wikipedia

<comment>Invisible in Internet Explorer. Plain visible text in Netscape.</comment>

<!-- The version that always worked -->
<!-- Invisible everywhere. -->
sandboxed demo · breaks nothing but itselfrestart

NOBR and WBR

dead tags 1995 partly #

Stop a line breaking, then mark the one place it may break after all.

In 2026: NOBR still works in every browser and is formally obsolete. WBR went the other way: it was a Netscape extension and it is now standard HTML. Two tags, same origin, opposite endings.

Where it came from: Netscape Navigator 1.1 extensions, 1995. WBR was standardised in HTML5.
MDN HTML standard

<nobr>This whole sentence refuses to wrap no matter how narrow the window gets.</nobr>

<p>A very long identifier like
<nobr>supercalifragilistic<wbr>expialidocious<wbr>andthensome</nobr>
gets to break only where you said it could.</p>

<!-- The standard way to say the same thing -->
<span style="white-space: nowrap">will not wrap</span>
sandboxed demo · breaks nothing but itselfrestart

The BASEFONT Tag

dead tags 1995 dead #

Set the font for the rest of the document, from a tag with no closing tag.

In 2026: Dead. Removed from HTML5 and no longer implemented. Sizes ran 1 to 7 with 3 as normal, which is also why you see size=+1 and size=-1 everywhere in old markup.

Where it came from: HTML 3.2, W3C, 1997. Deprecated in HTML 4.0, 1998.
MDN HTML 4.01

<basefont size="4" color="#000080" face="Arial, Helvetica, sans-serif">

<p>Everything after that tag inherits size 4 navy Arial.</p>
<p><font size="+1">One step bigger than the basefont.</font></p>
<p><font size="-1">One step smaller.</font></p>

<!-- The 2026 version -->
<style>body { font: 16px Arial, Helvetica, sans-serif; color: #000080 }</style>
sandboxed demo · breaks nothing but itselfrestart

DIR and MENU Lists

dead tags 1994 partly #

Two extra list types that always rendered exactly like an unordered list.

In 2026: DIR is gone from the spec and MENU came back in HTML5 for toolbars, then mostly went away again. Browsers render both as a plain bullet list, which is what they always did. This is redundancy that shipped and never earned its keep.

Where it came from: HTML 2.0 (RFC 1866), 1995. DIR deprecated in HTML 4.01.
MDN HTML 4.01

<!-- Multi-column directory listing, in theory -->
<dir>
  <li>readme.txt
  <li>index.html
  <li>logo.gif
</dir>

<!-- Single-line menu items, in theory -->
<menu>
  <li>File
  <li>Edit
  <li>Help
</menu>

<!-- Both render as this, and always did -->
<ul><li>File<li>Edit<li>Help</ul>
sandboxed demo · breaks nothing but itselfrestart

The Presentational Tag Zoo

dead tags 1995 partly #

Eight tags for making text look different, most of which say the same thing twice.

In 2026: They all still render, because breaking the old web is worse than carrying them. But B and I now mean something different from STRONG and EM, S and U are back with new meanings, and STRIKE, TT, BIG and FONT are gone from the spec.

Where it came from: HTML 3.2 and HTML 4.01, W3C, 1997 and 1999.
MDN HTML standard

<b>bold</b> and <strong>strong</strong> look identical but no longer mean the same thing.
<i>italic</i> and <em>emphasis</em>, likewise.
<strike>struck out</strike>, <s>also struck out</s>, and
<del>deleted</del> are three tags for one line through some words.
<tt>teletype</tt> and <code>code</code> and <kbd>keyboard</kbd> and
<samp>sample</samp> are four monospace tags.
<big>bigger</big> and <small>smaller</small>.
<u>underlined</u>, which for twenty years meant "this looks like a link but is not".
<font face="Comic Sans MS" size="5" color="#FF00FF">and the font tag</font>
sandboxed demo · breaks nothing but itselfrestart

The KEYGEN Tag

dead tags 1996 dead #

A form control that generated a public and private key pair in the browser.

In 2026: Removed from HTML and from every browser by 2019. Netscape shipped it for client certificate enrolment, and for twenty years it was the only way a web page could make a key pair. WebCrypto replaced it.

Where it came from: Netscape Navigator 3 client certificate documentation, 1996. Removed from the HTML standard in 2017.
MDN MDN Web Crypto

<form action="/cgi-bin/enroll.cgi" method="post">
  Your name: <input type="text" name="cn">
  Key strength: <keygen name="spkac" challenge="randomstring" keytype="rsa">
  <input type="submit" value="Request certificate">
</form>
sandboxed demo · breaks nothing but itselfrestart

The HR Tag's Lost Attributes

dead tags 1995 partly #

Four presentational attributes on the humble horizontal rule.

In 2026: Browsers still honour every one of these, but all four were dropped from the spec in HTML5. COLOR was Internet Explorer only and never worked in Netscape, which is why so many pages used a coloured 1 pixel gif instead.

Where it came from: HTML 3.2, W3C, 1997.
MDN HTML 4.01

<hr>
<hr size="1" noshade>
<hr size="8" width="50%" align="center">
<hr size="4" width="300" align="left" color="#FF0000">

<!-- The gif that people used instead, because HR COLOR was IE only -->
<img src="https://xbawx.com/inc/img/demo/redline.gif" width="300" height="4" alt="">

<!-- The 2026 version -->
<hr style="height:4px; width:300px; border:0; background:#FF0000">
sandboxed demo · breaks nothing but itselfrestart

The Spacer GIF

layout hacks 1995 partly #

A one pixel transparent gif, stretched, holding the entire layout apart.

In 2026: It still works, which is the problem. Every page of the period carried a dozen of these because tables had no reliable way to hold a fixed gap. Use padding.

Where it came from: Standard practice from about 1995. David Siegel's Creating Killer Web Sites, 1996, popularised it as the single pixel trick.
Wikipedia Wayback: killersites

<!-- The file itself: one transparent pixel, 43 bytes -->
<img src="https://xbawx.com/inc/img/demo/spacer.gif" width="1" height="20" alt="" border="0">

<!-- Holding a table column to exactly 150 pixels -->
<table cellpadding="0" cellspacing="0" border="0">
<tr>
  <td width="150"><img src="https://xbawx.com/inc/img/demo/spacer.gif" width="150" height="1" alt=""></td>
  <td>Content column</td>
</tr>
</table>

<!-- The 2026 version, using a data URI so you can see it work -->
<img alt="" height="20" width="1"
  src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
<span style="background:#333;color:#fff">the gap above is a spacer gif</span>
sandboxed demo · breaks nothing but itselfrestart

Table Layout

layout hacks 1996 still works #

Nested tables holding a page together, because CSS layout did not exist yet.

In 2026: Works fine, and that is the trouble: it renders identically today, so plenty of it is still out there. Screen readers announce it as a data table, and it reflows badly on a phone. Grid and flexbox replaced it.

Where it came from: Universal practice, roughly 1996 to 2006.
Wikipedia

<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
  <td colspan="2" bgcolor="#000080" height="60">
    <font face="Arial" color="#FFFFFF" size="5">&nbsp;MY HOME PAGE</font>
  </td>
</tr>
<tr valign="top">
  <td width="150" bgcolor="#CCCCCC">
    <table cellpadding="4" cellspacing="0" border="0" width="100%">
      <tr><td><a href="/">Home</a></td></tr>
      <tr><td><a href="/about.html">About</a></td></tr>
      <tr><td><a href="/links.html">Links</a></td></tr>
    </table>
  </td>
  <td bgcolor="#FFFFFF">
    <table cellpadding="10" cellspacing="0" border="0"><tr><td>
      Main content goes in here, three tables deep.
    </td></tr></table>
  </td>
</tr>
</table>
sandboxed demo · breaks nothing but itselfrestart

The Four Attribute Margin Reset

layout hacks 1997 partly #

Two attributes for Internet Explorer, two for Netscape, to get a page flush to the edge.

In 2026: Browsers still honour all four, and all four are obsolete. IE used TOPMARGIN and LEFTMARGIN, Netscape used MARGINHEIGHT and MARGINWIDTH, and nobody knew which visitor they had, so everyone shipped all four.

Where it came from: Microsoft and Netscape HTML extensions, both 1996. Neither was ever standard.
MDN

<html>
<head><title>My Home Page</title></head>
<body bgcolor="#000000" text="#00FF00" link="#FFFF00" vlink="#FF00FF" alink="#FF0000"
      topmargin="0" leftmargin="0" marginheight="0" marginwidth="0">
<font face="Verdana" size="2"><b>Flush against the corner.</b> No gap above,
no gap left: two attributes for IE, two for Netscape, all four obsolete and
all four still honoured.</font>

<!-- The 2026 version. Left inert here so the attributes above do the work. -->
<!-- <style>body { margin: 0; background: #000; color: #0f0 }</style> -->
</body>
</html>
sandboxed demo · breaks nothing but itselfrestart

The Box Model Hack

layout hacks 2002 dead #

A fake CSS property with an escaped quote in it, used to feed two widths to two browsers.

In 2026: No longer needed. Internet Explorer 5 measured width as border to border instead of content, so a box sized for one browser was wrong in the other. The hack works because IE5 choked on the escaped quote and stopped parsing the rule there.

Where it came from: Tantek Celik, devised around 2001 while he worked on IE5 for the Mac, published on tantek.com and widely reprinted around 2002.
Wikipedia tantek.com original

<style>
div.content {
  padding: 10px;
  border: 5px solid black;
  width: 400px;            /* what IE5 needs, border to border */
  voice-family: "\"}\"";   /* IE5 chokes here and stops reading the rule */
  voice-family: inherit;
  width: 370px;            /* what everyone else needs, content only */
}
</style>

<!-- The 2026 version. One line, no trickery. -->
<style>
div.content { box-sizing: border-box; width: 400px; padding: 10px; border: 5px solid #000 }
</style>
sandboxed demo · breaks nothing but itselfrestart

Star HTML and the Underscore Hack

layout hacks 2003 partly #

CSS aimed at one browser by exploiting a bug in how it parsed the selector.

In 2026: The selectors are still valid CSS so nothing errors, they just match nothing now. IE6 and below invented a phantom element above HTML, so * html matched only there. The underscore and star prefixes worked because IE ignored the leading character.

Where it came from: Widely circulated on css-discuss and A List Apart, roughly 2001 to 2005.
Wikipedia Wayback: Position Is Everything

<style>
/* Everyone */
.box { height: 100px; margin-left: 20px; }

/* IE6 and below only: they behave as if something wraps HTML */
* html .box { height: 120px; }

/* IE7 and below only */
*+html .box { height: 110px; }

/* IE6 only, inside a normal rule. Valid browsers drop the property. */
.box { width: 300px; _width: 320px; }

/* IE7 and below only, same trick with a star */
.box { padding: 10px; *padding: 12px; }
</style>

<div class="box" style="border:2px solid #333;background:#cfe;font:13px Verdana">
  A box the rules above all target. In a current browser only the plain rules
  apply, so nothing about it is hacked: the IE-only selectors match nothing.
</div>

<!-- The 2026 version: nothing. These browsers are gone. -->
sandboxed demo · breaks nothing but itselfrestart

zoom: 1 and hasLayout

layout hacks 2004 partly #

A meaningless property set purely to switch on an undocumented rendering mode.

In 2026: Harmless now: zoom is a real property again and 1 means no change. In IE6 and 7 an element either had layout or did not, which decided whether it contained floats, honoured width, or drew backgrounds at all. Nothing in CSS turned it on, so people used any property that happened to.

Where it came from: Reverse engineered by the CSS community, roughly 2004. Microsoft finally documented hasLayout on MSDN in 2005.
Wikipedia Wayback: On Having Layout

<style>
/* Any one of these flipped hasLayout on in IE6 and IE7 */
.fix { zoom: 1; }          /* the usual choice: no visible effect elsewhere */
.fix { height: 1%; }       /* the other usual choice */
.fix { display: inline-block; }
.fix { position: absolute; }
.fix { float: left; }
</style>

<div class="fix">
  Without one of these, IE6 would refuse to wrap this box around its floats,
  drop the background, or ignore the width outright.
</div>
sandboxed demo · breaks nothing but itselfrestart

Hiding CSS From Netscape 4

layout hacks 1998 partly #

An import statement used as a browser filter, because Netscape 4 could not read them.

In 2026: Still valid CSS and still works, though there is nothing left to hide from. Netscape 4 applied stylesheets badly enough that hiding them entirely produced a better page than letting it try.

Where it came from: Widely used from about 1998. Documented in the Web Standards Project's Netscape 4 upgrade campaign, 2001.
Wikipedia Wayback: WaSP upgrade

<!-- Netscape 4 reads this and applies the basics -->
<link rel="stylesheet" type="text/css" href="basic.css">

<!-- Netscape 4 cannot parse @import, so it never sees this one -->
<style type="text/css">
  @import url("modern.css");
</style>

<!-- The other half of the trick: comment wrapping, for Netscape 2 -->
<style type="text/css">
<!--
  body { background: #fff }
-->
</style>

<p style="font:13px Verdana">Netscape 4 loaded basic.css and never saw the
@import. Neither file exists in this demo, so this text is plain either way:
the exhibit is the filter, not the paint.</p>
sandboxed demo · breaks nothing but itselfrestart

The Clearfix

layout hacks 2004 still works #

An empty generated period, hidden, purely to make a container wrap its floats.

In 2026: Still works and still gets used, though display: flow-root now does the same thing honestly. The visibility: hidden period was there because a truly empty string did not generate a box in some browsers.

Where it came from: Tony Aslett, csscreator.com, 2004. Refined many times after.
MDN Wayback: Position Is Everything CSS-Tricks

<style>
.clearfix:after {
  content: ".";
  display: block;
  height: 0;
  clear: both;
  visibility: hidden;
}
* html .clearfix { height: 1%; }   /* and hasLayout for IE6 */
</style>

<div class="clearfix" style="border:2px solid #333">
  <div style="float:left; width:80px; height:60px; background:#8cf">float</div>
  <div style="float:left; width:80px; height:60px; background:#fc8">float</div>
</div>

<!-- The 2026 version -->
<style>.modern { display: flow-root }</style>
sandboxed demo · breaks nothing but itselfrestart

Conditional Comments

browser wars 1999 dead #

A comment that only one browser could see inside.

In 2026: Dead since Internet Explorer 10 dropped support in 2012. Every other browser saw a plain comment, which is what made it the only browser filter that was not a parser bug. The downlevel-revealed form was invalid HTML on purpose.

Where it came from: Microsoft Internet Explorer 5 documentation, 1999. The HTML class pattern is Paul Irish's, 2008.
Wikipedia Paul Irish

<!--[if IE]>          <p>Any Internet Explorer</p>          <![endif]-->
<!--[if IE 6]>        <p>Exactly IE 6</p>                   <![endif]-->
<!--[if lt IE 9]>     <script src="html5shiv.js"></script>  <![endif]-->
<!--[if gte IE 5.5]>  <p>IE 5.5 and up</p>                  <![endif]-->
<!--[if !IE]> -->     <p>Everyone except IE</p>        <!-- <![endif]-->

<!-- The pattern that ended up on nearly every page after 2010 -->
<!--[if lt IE 7]> <html class="no-js ie6"> <![endif]-->
<!--[if IE 7]>    <html class="no-js ie7"> <![endif]-->
<!--[if IE 8]>    <html class="no-js ie8"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
sandboxed demo · breaks nothing but itselfrestart

JScript Conditional Compilation

browser wars 1997 dead #

Code hidden inside a comment that one engine compiled and every other ignored.

In 2026: Dead with Internet Explorer. To everyone else this is a comment, so the script is safe to ship. It was the neatest browser filter of the lot and almost nobody used it.

Where it came from: Microsoft JScript 3.0 documentation, 1997.
Wikipedia

<script type="text/javascript">
var engine = "not JScript";
/*@cc_on
  @if (@_jscript_version >= 5.5)
    engine = "JScript " + @_jscript_version;
  @else
    engine = "old JScript";
  @end
@*/
document.write("Script engine: " + engine);
</script>
sandboxed demo · breaks nothing but itselfrestart

VBScript In A Web Page

browser wars 1996 dead #

Microsoft shipped a second scripting language in the browser and a few intranets used it.

In 2026: Dead outside Internet Explorer, and dead inside it since IE11 turned it off in 2019. Every browser that is not IE has always ignored the block entirely, so the demo below just does nothing.

Where it came from: Microsoft Internet Explorer 3 scripting documentation, 1996.
Wikipedia

<script language="VBScript">
Sub GreetButton_OnClick
  MsgBox "Hello from VBScript", 64, "Internet Explorer only"
End Sub

Sub Window_OnLoad
  Document.Write "<p>This page was written by VBScript.</p>"
End Sub
</script>

<input type="button" name="GreetButton" value="Click me (IE only)">

<!-- And the reason nobody could rely on it -->
<script language="JavaScript">
document.write("<p>JavaScript ran. VBScript almost certainly did not.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Versioned Script Language Attributes

browser wars 1996 partly #

Declaring which JavaScript you wrote, so older engines would skip the block.

In 2026: Modern browsers ignore the version and run every block, which is the opposite of what this was for. In 1996 Netscape 2 ran only the plain block, Netscape 3 ran up to 1.1 and so on, so people shipped the same function two or three times.

Where it came from: Netscape JavaScript Guide, 1996. The LANGUAGE attribute was deprecated in HTML 4.01 in favour of TYPE.
MDN

<script language="JavaScript">
  // Everyone, including Netscape 2
  var mode = "basic";
</script>

<script language="JavaScript1.1">
  // Netscape 3 and up. Image preloading arrived here.
  mode = "1.1: new Image() available";
</script>

<script language="JavaScript1.2">
  // Netscape 4 and up. Layers and captureEvents arrived here.
  mode = "1.2: layers available";
</script>

<script language="JavaScript">
  document.write("Engine reported: " + mode);
  document.write("<br>In 2026 every block above runs, so you get the last one.");
</script>
sandboxed demo · breaks nothing but itselfrestart

The Cross Browser DHTML Shim

browser wars 1998 still works #

The three way branch that sat at the top of every DHTML script for four years.

In 2026: The getElementById branch is the only one that runs now, and it is the only one you need. Netscape 4 used document.layers with its own coordinate properties, IE4 used document.all, and the two disagreed about almost everything.

Where it came from: The standard opening of DHTML scripts on Dynamic Drive and Webmonkey, roughly 1998 to 2002.
MDN

<div id="box" style="position:absolute; left:20px; top:60px; width:160px;
     background:#FFFFCC; border:1px solid #999">Move me.</div>

<script language="JavaScript">
// 1998, in full
var isNS4 = (document.layers) ? true : false;
var isIE4 = (document.all && !document.getElementById) ? true : false;
var isDOM = (document.getElementById) ? true : false;

function getObj(id) {
  if (isDOM) return document.getElementById(id).style;
  if (isIE4) return document.all[id].style;
  if (isNS4) return document.layers[id];       // note: not .style
  return null;
}
function moveTo(id, x, y) {
  var o = getObj(id);
  if (!o) return;
  if (isNS4) { o.left = x; o.top = y; }        // numbers
  else       { o.left = x + "px"; o.top = y + "px"; }   // strings with units
}

var n = 0;
setInterval(function () { n = (n + 4) % 160; moveTo("box", 20 + n, 60); }, 60);
</script>
sandboxed demo · breaks nothing but itselfrestart

SCRIPT FOR and EVENT

browser wars 1997 dead #

Attaching a handler by naming the element and the event on the script tag itself.

In 2026: Internet Explorer only, and gone with it. A spec-compliant browser skips a classic script that carries a for/event pair like this, running it only when for is window and event is a load event, so the handler below never fired outside Internet Explorer. The engines that did run it threw instead.

Where it came from: Microsoft Internet Explorer 4 DHTML reference, 1997.
MDN

<input type="button" id="btn" value="Click me">

<!-- IE only. A spec-compliant browser skips a for/event script entirely. -->
<script for="btn" event="onclick" language="JavaScript">
  alert("Handled by the FOR and EVENT attributes.");
</script>

<!-- What everybody should have written -->
<script language="JavaScript">
document.getElementById("btn").onclick = function () {
  alert("Handled the portable way.");
};
</script>
sandboxed demo · breaks nothing but itselfrestart

CSS expression()

browser wars 2000 dead #

JavaScript inside a stylesheet, re-evaluated by Internet Explorer on almost every event.

In 2026: Dead: removed in IE8 standards mode and never implemented anywhere else. It was the only way to fake min-width and max-width in IE6, and it was slow enough to make a page crawl, because the expression re-ran on scroll, resize and mouse move.

Where it came from: Microsoft dynamic properties, MSDN, 1998. Disabled by default in IE8, 2009.
Wikipedia

<style>
/* Fake max-width in IE6 */
.wrap {
  width: expression(document.body.clientWidth > 800 ? "800px" : "auto");
}

/* Fake min-height */
.panel {
  height: expression(this.scrollHeight < 200 ? "200px" : "auto");
}

/* Centre a fixed element, which IE6 also could not do */
.float {
  position: absolute;
  top: expression(document.body.scrollTop + 100 + "px");
}
</style>

<!-- The 2026 version -->
<style>.wrap { max-width: 800px } .panel { min-height: 200px } .float { position: fixed }</style>
sandboxed demo · breaks nothing but itselfrestart

The DXImageTransform Filters

browser wars 1998 dead #

Photoshop effects as CSS properties, five years before anyone else had any.

In 2026: Dead: removed in IE10. Some of it was genuinely ahead of its time, drop shadows and gradients and rotation years before the standards caught up, and some of it was a wave filter that made your headings ripple.

Where it came from: Microsoft Internet Explorer 4 and 5.5 visual filters, MSDN, 1998 and 2000.
Wikipedia

<style>
/* Opacity, before opacity */
.faded { filter: alpha(opacity=50); -moz-opacity: 0.5; opacity: 0.5; }

/* Gradient, before gradients */
.bar {
  filter: progid:DXImageTransform.Microsoft.gradient(
          startColorstr='#FFFF0000', endColorstr='#FF0000FF');
}

/* Drop shadow, before box-shadow */
.card { filter: progid:DXImageTransform.Microsoft.Shadow(
        color='#808080', Direction=135, Strength=4); }

/* Glow, and the one everybody regrets */
.hot  { filter: progid:DXImageTransform.Microsoft.Glow(color=#FFFF00, strength=6); }
.wavy { filter: progid:DXImageTransform.Microsoft.Wave(
        add=0, freq=3, lightstrength=20, phase=0, strength=4); }

/* The genuinely useful one: alpha PNGs in IE6, which had none */
.logo { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(
        src='https://xbawx.com/inc/img/demo/logo.png', sizingMethod='image'); }
</style>

<!-- The 2026 version -->
<style>
.faded { opacity: .5 }
.bar   { background: linear-gradient(#f00, #00f) }
.card  { box-shadow: 4px 4px 0 #808080 }
.hot   { text-shadow: 0 0 8px #ff0 }
</style>
sandboxed demo · breaks nothing but itselfrestart

Coloured Scrollbars

browser wars 2000 dead #

Seven CSS properties for repainting the browser's own scrollbar.

In 2026: Dead in IE, and the modern replacements are different again: scrollbar-color and scrollbar-width in Firefox, and the ::-webkit-scrollbar pseudo-elements in Chrome. Nobody has agreed on this in twenty five years.

Where it came from: Microsoft Internet Explorer 5.5 documentation, 2000.
MDN

<style>
body {
  scrollbar-face-color:       #000080;
  scrollbar-arrow-color:      #FFFF00;
  scrollbar-track-color:      #000040;
  scrollbar-shadow-color:     #0000FF;
  scrollbar-highlight-color:  #6666FF;
  scrollbar-3dlight-color:    #9999FF;
  scrollbar-darkshadow-color: #000020;
}
</style>

<!-- The 2026 versions, still two of them -->
<style>
html { scrollbar-color: #ff0 #000080; scrollbar-width: thin; }
::-webkit-scrollbar { width: 12px }
::-webkit-scrollbar-track { background: #000040 }
::-webkit-scrollbar-thumb { background: #000080 }
</style>
sandboxed demo · breaks nothing but itselfrestart

Meta Refresh

meta and head 1995 partly #

A redirect, a slideshow and a page reloader, all from one tag in the head.

In 2026: Still works in every browser, and after decades outside every spec HTML5 finally standardised it. It breaks the back button and it gives the visitor no way to stop, which is why a server 301 is the right answer for a redirect. Only the no-URL form runs here, so the clock below jumps every four seconds. The two redirect versions are shown as inert markup, because a live one would pull the demo straight off to another page.

Where it came from: Netscape Navigator 1.1 client pull, 1995. It stayed outside every HTML spec until HTML5 finally standardised the refresh pragma.
Wikipedia HTML standard

<!-- Redirect after 5 seconds. Left inert here: a live redirect drags the demo away. -->
<!-- <meta http-equiv="refresh" content="5;url=http://www.example.com/newpage.html"> -->

<!-- Redirect immediately, the poor man's 301. Also inert here. -->
<!-- <meta http-equiv="refresh" content="0;url=/index.html"> -->

<!-- No URL: reload this same page. Slideshows and webcams used this. This one is live. -->
<meta http-equiv="refresh" content="4">

<script language="JavaScript">
document.write("<p>Loaded at " + new Date().toLocaleTimeString() +
  ". Watch the clock jump every four seconds.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

The Meta Tag Cargo Cult

meta and head 1997 dead #

Eleven meta tags nobody ever read, copied from page to page for a decade.

In 2026: None of these do anything, and most never did. REVISIT-AFTER in particular was invented by one small search engine, ignored by all the rest, and pasted into millions of pages anyway. Search engines say so in their own documentation.

Where it came from: Origin unclear, which is the point. Google's webmaster documentation has said for years that it ignores nearly all of these.
MDN Google, 2009

<meta name="revisit-after" content="7 days">
<meta name="distribution" content="global">
<meta name="rating" content="general">
<meta name="robots" content="index, follow">
<meta name="classification" content="Personal Homepage">
<meta name="resource-type" content="document">
<meta name="doc-class" content="Living Document">
<meta name="copyright" content="Copyright 1998">
<meta http-equiv="expires" content="never">
<meta http-equiv="pragma" content="no-cache">
<meta name="generator" content="Microsoft FrontPage 4.0">

<!-- The three that actually matter in 2026 -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="A real description of this page.">
sandboxed demo · breaks nothing but itselfrestart

Keyword Stuffing

meta and head 1996 dead #

The meta tag that decided search rankings until people worked out they could just lie.

In 2026: Dead as a ranking signal. Google has stated publicly since 2009 that it does not use the keywords meta tag at all, and it stopped mattering years before that, for the obvious reason.

Where it came from: Common practice from 1996. Google confirmed it ignores the keywords tag in a Webmaster Central post, September 2009.
Wikipedia Google, 2009

<meta name="keywords" content="home page, homepage, personal, cool links, free,
  free stuff, mp3, mp3s, games, best site, cool site, awesome, links, web,
  internet, surf, surfing, guestbook, sign my guestbook, under construction">

<meta name="description" content="Welcome to my home page!!! Sign my guestbook!!!">

<!-- Some pages also hid a wall of the same words in the body -->
<font color="#FFFFFF" size="1">free mp3 games cool links free mp3 games cool links</font>
sandboxed demo · breaks nothing but itselfrestart

Internet Explorer Only Meta Tags

meta and head 1999 dead #

Four meta tags that only Internet Explorer read, most of them switching off something it did to your page uninvited.

In 2026: All dead with the browser. IMAGETOOLBAR turned off the floating save and print buttons IE6 drew over any image wider than 200 pixels. MSSmartTagsPreventParsing was the web's response to Microsoft planning to insert its own links into other people's text.

Where it came from: MSDN, 1999 to 2001. The Smart Tags plan was announced and abandoned in 2001 after a public outcry.
MDN Wikipedia Smart tags

<!-- Stop IE6 drawing save and print buttons over your images -->
<meta http-equiv="imagetoolbar" content="no">

<!-- Let form controls use the Windows XP theme -->
<meta http-equiv="MSThemeCompatible" content="yes">

<!-- Refuse Smart Tags, the 2001 plan to auto-link words in your pages -->
<meta name="MSSmartTagsPreventParsing" content="true">

<!-- And the one that outlived them, forcing IE out of compatibility mode -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
sandboxed demo · breaks nothing but itselfrestart

The APPLET Tag

plugins and media 1996 dead #

Java in the page. Ripple effects, fire text, and a navigation menu that took eight seconds to appear.

In 2026: Dead. Chrome dropped the plugin API in 2015, Firefox followed in 2017, and no modern browser can run an applet at all. The tag itself was deprecated in HTML 4.01 in favour of OBJECT, which then also died.

Where it came from: Sun Microsystems Java applet documentation, 1996. Anfy applets by Fabio Ciucci, anfyteam.com, 1997 onward.
Wikipedia Wayback: Anfy Team Chromium blog

<!-- The water ripple effect, on roughly one in five pages in 1998 -->
<applet code="Lake.class" archive="lake.jar" width="400" height="300">
  <param name="image"  value="images/logo.jpg">
  <param name="ripples" value="6">
  <param name="halfsize" value="YES">
  <p>Your browser does not support Java. This is where the lake went.</p>
</applet>

<!-- Anfy, the shareware applet pack everybody used -->
<applet code="anfy.class" codebase="anfy/" width="320" height="200">
  <param name="credits" value="Applet by Fabio Ciucci">
  <param name="reg" value="unregistered">
</applet>

<!-- The HTML 4 replacement, which also died -->
<object classid="java:Lake.class" width="400" height="300">
  <param name="image" value="images/logo.jpg">
</object>
sandboxed demo · breaks nothing but itselfrestart

The Flash Twin Tags

plugins and media 1998 dead #

OBJECT for Internet Explorer, EMBED nested inside it for everyone else, both pointing at the same file.

In 2026: Dead: Flash was switched off worldwide at the end of 2020. The nesting worked because IE read the OBJECT and ignored the EMBED, while Netscape did the reverse. The CODEBASE URL fetched the plugin installer if the visitor had none.

Where it came from: Macromedia Flash 4 publishing templates, 1999. The nesting pattern is often credited to Macromedia's own HTML export.
Wikipedia Wayback: Adobe Flash EOL

<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
        codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0"
        width="550" height="400" id="intro">
  <param name="movie" value="intro.swf">
  <param name="quality" value="high">
  <param name="bgcolor" value="#000000">
  <param name="wmode" value="transparent">
  <embed src="intro.swf" quality="high" bgcolor="#000000" wmode="transparent"
         width="550" height="400" name="intro" type="application/x-shockwave-flash"
         pluginspage="http://www.macromedia.com/go/getflashplayer">
</object>

<!-- And the skip link, because everybody hated the intro -->
<p><a href="main.html">Skip intro</a></p>
sandboxed demo · breaks nothing but itselfrestart

Video Inside An IMG Tag

plugins and media 1996 dead #

Internet Explorer let you point an image at an AVI file and play it on mouseover.

In 2026: Dead, and it only ever worked in Internet Explorer on Windows. Netscape users saw the still from the SRC attribute, which is why every page using it needed both. This is the closest the 90s web got to a video tag.

Where it came from: Microsoft Internet Explorer 2 HTML extensions, 1996.
MDN

<!-- IE plays the AVI. Everyone else sees still.gif. -->
<img src="https://xbawx.com/inc/img/demo/photo.jpg" dynsrc="clips/wave.avi"
     start="mouseover" loop="infinite" controls
     width="320" height="240" alt="Waving">

<!-- start="fileopen" played it as soon as the page loaded -->
<img src="https://xbawx.com/inc/img/demo/photo.jpg" dynsrc="clips/intro.avi"
     start="fileopen" loop="2" width="320" height="240" alt="">

<!-- The 2026 version -->
<video src="clips/wave.mp4" poster="https://xbawx.com/inc/img/demo/photo.jpg" loop muted controls
       width="320" height="240"></video>
sandboxed demo · breaks nothing but itselfrestart

The LOWSRC Attribute

plugins and media 1995 dead #

Show a tiny placeholder first, then swap in the real photo when it finally arrives.

In 2026: Dead: dropped from browsers, though the idea came back as progressive JPEG and then as modern lazy loading with a blurred placeholder. On a 28.8k modem a 40k photo took twelve seconds, so this mattered.

Where it came from: Netscape Navigator 1.1 HTML extensions, 1995.
MDN

<img src="https://xbawx.com/inc/img/demo/photo.jpg" lowsrc="https://xbawx.com/inc/img/demo/photo_tiny.jpg"
     width="400" height="300" alt="Holiday photo">

<!-- The 2026 version of the same idea -->
<img src="https://xbawx.com/inc/img/demo/photo.jpg" width="400" height="300" loading="lazy"
     alt="Holiday photo"
     style="background: url(https://xbawx.com/inc/img/demo/photo_tiny.jpg) center/cover">
sandboxed demo · breaks nothing but itselfrestart

RealAudio Metafiles

plugins and media 1996 dead #

You did not link the audio. You linked a text file containing the address of the audio.

In 2026: Dead with RealPlayer. The indirection existed because the browser would have downloaded a .ra file instead of streaming it, so the link pointed at a one line .ram whose MIME type handed the whole thing to the plugin.

Where it came from: RealNetworks RealAudio content creation guide, 1996.
Wikipedia Wayback: RealAudio

<!-- song.ram is a text file containing exactly one line: -->
<!--   pnm://media.example.com/music/song.ra                -->

<a href="song.ram">Listen to my song (RealAudio, 28.8k)</a>
<a href="song_isdn.ram">Listen (ISDN, 56k or better)</a>

<!-- Embedded, with a control panel drawn by the plugin -->
<embed src="song.rpm" type="audio/x-pn-realaudio-plugin"
       controls="ControlPanel" width="275" height="30" autostart="false">
<embed src="song.rpm" type="audio/x-pn-realaudio-plugin"
       controls="StatusBar" width="275" height="30" nolabels="true">

<!-- The 2026 version -->
<audio src="https://xbawx.com/snd/preview/70/70db912c2b37cc4682e540b712576f1338089503.mp3" controls></audio>
sandboxed demo · breaks nothing but itselfrestart

The Fixed Background Watermark

plugins and media 1996 dead #

One Internet Explorer attribute that stopped the tiled background scrolling with the page.

In 2026: Dead as an attribute, but the effect became CSS background-attachment: fixed, which works everywhere. Netscape users just got a scrolling tile, and mostly nobody noticed.

Where it came from: Microsoft Internet Explorer 2 HTML extensions, 1996.
MDN

<!-- IE only: the tile stays put while the text scrolls over it -->
<body bgproperties="fixed" bgcolor="#000000"
      background="https://xbawx.com/btn/assets/d3/d3ae4d9ccdfb98981c463eb3945df5ff2748d551.gif">

<!-- The 2026 version, in every browser -->
<style>
body {
  background-image: url(https://xbawx.com/btn/assets/d3/d3ae4d9ccdfb98981c463eb3945df5ff2748d551.gif);
  background-attachment: fixed;
  background-color: #000;
}
</style>
sandboxed demo · breaks nothing but itselfrestart

A Frameset Page

frames 1996 partly #

A page with no body, holding three other pages in a grid.

In 2026: Frames still render, and they were removed from the HTML standard anyway. They broke bookmarking, printing, the back button and search results, all at once. The demo below really is a frameset and really does split into three panes. The panes are empty because the frame sources are relative paths that do not exist in the sandbox.

Where it came from: Netscape Navigator 2.0 frames extension, 1996. Standardised in HTML 4.0 in a separate frameset DTD, then removed in HTML5.
MDN HTML 4.01 frames

<html>
<head><title>My Home Page</title></head>
<frameset rows="80,*" frameborder="0" border="0" framespacing="0">
  <frame name="banner" src="banner.html" scrolling="no" noresize marginheight="0">
  <frameset cols="150,*">
    <frame name="nav"  src="nav.html"  scrolling="auto" marginwidth="0">
    <frame name="main" src="main.html" scrolling="auto">
  </frameset>
  <noframes>
  <body bgcolor="#FFFFFF">
    <p>This site uses frames. <a href="main.html">Here is the no frames version.</a></p>
  </body>
  </noframes>
</frameset>
</html>
sandboxed demo · breaks nothing but itselfrestart

Frame Targets and BASE TARGET

frames 1996 partly #

Naming a frame so links in one pane load in another, and the four magic target names.

In 2026: The reserved targets still work. _new was never one of them: people used it constantly as a synonym for _blank, and browsers treated it as an ordinary window name, so every link with target=_new after the first reused the same window.

Where it came from: Netscape Navigator 2.0 frames documentation, 1996. rel=noopener was added to the HTML standard in 2016.
MDN

<!-- Every link on this page loads into the frame named main -->
<base target="main">

<a href="page1.html">Loads in the main frame</a>
<a href="page2.html" target="_self">Loads in this frame</a>
<a href="page3.html" target="_parent">Loads in the frameset that holds this one</a>
<a href="page4.html" target="_top">Breaks out of all frames</a>
<a href="page5.html" target="_blank">New window</a>

<!-- The one that was never real -->
<a href="page6.html" target="_new">Not a reserved name. This is just a window called _new.</a>

<!-- The 2026 note: pair _blank with rel, or the new page can reach back -->
<a href="page7.html" target="_blank" rel="noopener noreferrer">Safe new tab</a>
sandboxed demo · breaks nothing but itselfrestart

The IFRAME's Presentational Attributes

frames 1997 partly #

Five attributes for controlling an inline frame's chrome, all now obsolete.

In 2026: Browsers still honour most of them and the HTML standard lists them as obsolete. ALLOWTRANSPARENCY was Internet Explorer only and was the only way to see through a frame to the page behind it.

Where it came from: Microsoft Internet Explorer 3 HTML extensions, 1996. Standardised in HTML 4.0.
MDN

<iframe src="inner.html"
        width="400" height="200"
        frameborder="0"
        scrolling="no"
        marginwidth="0" marginheight="0"
        allowtransparency="true"
        name="inner">
  Your browser does not support inline frames.
</iframe>

<!-- The 2026 version. srcdoc means there is no second file to fetch, which
     is also why this one has something in it and the one above does not. -->
<iframe width="400" height="120" style="border:0" loading="lazy"
        sandbox="allow-scripts" title="Inner page"
        srcdoc="&lt;body style='font:13px Verdana;margin:8px'&gt;
                Inline frame content, no border, no scrollbars.&lt;/body&gt;"></iframe>
sandboxed demo · breaks nothing but itselfrestart

The Mailto Form

forms and protocols 1996 partly #

A form that posted straight to an email address, with no CGI script behind it.

In 2026: Partly, and less dead than its reputation. Mailto links themselves are fine, and the HTML standard still defines what a mailto form does: GET puts the fields in the mailto URL, POST with enctype text/plain hands them over as the message body, and the browser opens the visitor's mail client with the data filled in. Nothing sends itself, though: with no mail client configured it silently does nothing, and the visitor still presses Send from their own address. That unreliability, plus Netscape and IE encoding the body differently, is why every free host offered a formmail CGI instead.

Where it came from: HTML 3.2 allowed any URL as a form action. FormMail.pl by Matt Wright, Matt's Script Archive, 1995.
Wikipedia HTML standard Matt's Script Archive

<form action="mailto:you@example.com" method="post" enctype="text/plain">
  Name:    <input type="text" name="name"><br>
  Comment: <textarea name="comment" rows="4" cols="30"></textarea><br>
  <input type="submit" value="Send"> <input type="reset" value="Clear">
</form>

<!-- The GET variant. Submitting REPLACES the query string, so the subject
     here is lost and msg arrives as a header most mail clients ignore. -->
<form action="mailto:you@example.com?subject=From my web page" method="get">
  <input type="text" name="msg"><input type="submit" value="Send">
</form>

<!-- What everybody actually used -->
<form action="/cgi-bin/formmail.pl" method="post">
  <input type="hidden" name="recipient" value="you@example.com">
  <input type="hidden" name="subject" value="Web form">
  <input type="hidden" name="redirect" value="http://example.com/thanks.html">
  <input type="text" name="realname"><input type="submit" value="Send">
</form>
sandboxed demo · breaks nothing but itselfrestart

Image Maps, Both Kinds

forms and protocols 1994 partly #

One picture, several links, decided either by the browser or by a CGI script on the server.

In 2026: Client side maps still work and are still in the standard. Server side maps, the ISMAP attribute, sent the click coordinates to the server as a query string and needed a map file and a CGI handler, and are effectively dead. Nearly every 90s navigation bar was one of these.

Where it came from: Server side maps: NCSA httpd imagemap, 1993. Client side maps: HTML 3.2, 1997.
MDN

<!-- Client side: the browser decides -->
<img src="https://xbawx.com/inc/img/demo/navbar.gif" usemap="#nav" border="0" width="400" height="40" alt="Navigation">
<map name="nav">
  <area shape="rect"   coords="0,0,100,40"    href="/"            alt="Home">
  <area shape="rect"   coords="100,0,200,40"  href="/about.html"  alt="About">
  <area shape="circle" coords="250,20,18"     href="/links.html"  alt="Links">
  <area shape="poly"   coords="300,0,400,0,350,40" href="/gb.html" alt="Guestbook">
  <area shape="default" nohref>
</map>

<!-- Server side: the browser sends the pixel it was clicked on -->
<a href="/cgi-bin/imagemap/nav.map">
  <img src="https://xbawx.com/inc/img/demo/navbar.gif" ismap border="0" width="400" height="40" alt="">
</a>

<!-- nav.map on the server looked like this -->
<!--   default /index.html                                   -->
<!--   rect    /about.html   100,0  200,40                   -->
<!--   circle  /links.html   250,20 268,20                   -->
sandboxed demo · breaks nothing but itselfrestart

INPUT TYPE=IMAGE

forms and protocols 1995 still works #

A submit button that is a picture, and quietly sends the click coordinates too.

In 2026: Still standard and still works. The surprise is the payload: it submits name.x and name.y rather than name and value, which broke a great many server scripts that expected a plain button.

Where it came from: HTML 2.0 (RFC 1866), 1995.
MDN

<form action="/cgi-bin/search.pl" method="get">
  <input type="text" name="q" size="20">
  <input type="image" src="https://xbawx.com/inc/img/demo/go.gif" name="go" border="0" width="40" height="20"
         alt="Search">
</form>

<!-- What the server actually receives -->
<!--   q=cats&go.x=17&go.y=9                                     -->
<!--   Note: no "go" parameter at all, which surprised everybody. -->

<!-- The 2026 version, if you just want a styled submit -->
<button type="submit"><img src="https://xbawx.com/inc/img/demo/go.gif" alt="Search"></button>
sandboxed demo · breaks nothing but itselfrestart

The TEXTAREA WRAP Attribute

forms and protocols 1996 partly #

Three wrap modes, none of them standard by those names, and one of which changed what got sent.

In 2026: Browsers still accept them and HTML5 standardised soft and hard. VIRTUAL wrapped on screen but sent one long line, PHYSICAL inserted real line breaks into the submitted data, and OFF did not wrap at all. Getting this wrong mangled every guestbook entry.

Where it came from: Netscape Navigator 2.0 forms extensions, 1996. Standardised in HTML5.
MDN

<!-- Netscape's three modes -->
<textarea name="c" rows="4" cols="30" wrap="virtual">Wraps on screen, sends one line.</textarea>
<textarea name="c" rows="4" cols="30" wrap="physical">Wraps on screen, sends the breaks.</textarea>
<textarea name="c" rows="4" cols="30" wrap="off">No wrapping. Scrolls sideways forever.</textarea>

<!-- The two that HTML5 kept -->
<textarea name="c" rows="4" cols="30" wrap="soft">Sends one line. Same as virtual.</textarea>
<textarea name="c" rows="4" cols="30" wrap="hard">Sends the breaks. Needs cols set.</textarea>
sandboxed demo · breaks nothing but itselfrestart

Server Side Includes

server side 1995 dead #

Directives hidden in HTML comments that the web server acted on before sending the page.

In 2026: Mostly gone. The browser sees a plain comment, which is exactly what you get below, because the server never processed it. SSI was how you had a shared header before anyone had a templating language, and it usually needed the file renamed to .shtml.

Where it came from: NCSA httpd server side includes, 1995. Carried into Apache mod_include.
Wikipedia Apache mod_include

<!--#include virtual="/inc/header.html" -->

<h1>My Page</h1>

<!--#config timefmt="%A, %B %d, %Y" -->
<p>Today is <!--#echo var="DATE_LOCAL" -->.</p>
<p>This file was last changed <!--#flastmod file="index.shtml" -->.</p>
<p>You came from <!--#echo var="HTTP_REFERER" -->.</p>
<p>Your browser is <!--#echo var="HTTP_USER_AGENT" -->.</p>

<!-- The hit counter, run as a program on every page load -->
You are visitor number <!--#exec cgi="/cgi-bin/counter.cgi" -->

<!--#include virtual="/inc/footer.html" -->
sandboxed demo · breaks nothing but itselfrestart

FrontPage WebBots

server side 1997 dead #

Microsoft FrontPage wrote its own comment directives into your HTML and needed its own server extensions to run them.

In 2026: Dead with FrontPage and its server extensions. The browser sees comments and the cached value between them, which is why an unmaintained page can still show a last updated date from 1999. The i-checksum was there to catch you editing it by hand.

Where it came from: Microsoft FrontPage 97 and 98 documentation.
Wikipedia

<!--webbot bot="Timestamp" S-Type="EDITED" S-Format="%m/%d/%y" startspan -->01/14/99<!--webbot bot="Timestamp" endspan i-checksum="12345" -->

<!--webbot bot="HTMLMarkup" startspan -->
<img src="https://xbawx.com/counter/3cswt32t59r8.png" alt="hit counter">
<!--webbot bot="HTMLMarkup" endspan -->

<!--webbot bot="Include" U-Include="../_private/nav.htm" TAG="BODY" startspan -->
  ... the included file's content was pasted here by the editor ...
<!--webbot bot="Include" endspan -->

<!--webbot bot="PurpleText" preview="Note to self: fix the guestbook link" -->

<meta name="GENERATOR" content="Microsoft FrontPage 4.0">
<meta name="ProgId" content="FrontPage.Editor.Document">
sandboxed demo · breaks nothing but itselfrestart

HTML Saved From Microsoft Word

server side 1999 partly #

Forty kilobytes of markup for a page of text, most of it describing a print layout nobody asked for.

In 2026: It still renders, which is the whole problem. The o: and mso- names are XML namespaces Word invented for itself. The empty paragraph tag o:p exists because Word needed somewhere to hang a paragraph mark.

Where it came from: Microsoft Word 97 and 2000 Save as Web Page output.
Wikipedia

<html xmlns:o="urn:schemas-microsoft-com:office:office"
      xmlns:w="urn:schemas-microsoft-com:office:word">
<head>
<meta name=Generator content="Microsoft Word 9">
<!--[if gte mso 9]><xml>
 <o:DocumentProperties><o:Author>Dad</o:Author></o:DocumentProperties>
</xml><![endif]-->
<style>
 p.MsoNormal { mso-style-parent:""; font-size:12.0pt; font-family:"Times New Roman";
               mso-fareast-font-family:"Times New Roman"; }
 @page Section1 { size:8.5in 11.0in; margin:1.0in 1.25in 1.0in 1.25in; }
</style>
</head>
<body lang=EN-US style='tab-interval:.5in'>
<div class=Section1>
<p class=MsoNormal><span style='mso-spacerun:yes'>&nbsp;&nbsp;</span>
  Hello, this is my web page.<o:p></o:p></p>
<p class=MsoNormal><o:p>&nbsp;</o:p></p>
</div>
</body>
</html>
sandboxed demo · breaks nothing but itselfrestart

Dreamweaver MM_ Behaviors

server side 1999 still works #

Four functions with a vendor prefix, pasted into the head of a very large number of pages.

In 2026: They still work: it is ordinary JavaScript. MM_findObj carries the whole browser history of the period in one function, checking document.layers, document.all and document.getElementById in turn, plus a branch for finding things inside frames.

Where it came from: Macromedia Dreamweaver 3 and 4 behaviors, 1999 and 2000.
Wikipedia

<script language="JavaScript">
function MM_findObj(n, d) { //v4.01
  var p, i, x;
  if (!d) d = document;
  if ((p = n.indexOf("?")) > 0 && parent.frames.length) {
    d = parent.frames[n.substring(p + 1)].document; n = n.substring(0, p);
  }
  if (!(x = d[n]) && d.all) x = d.all[n];
  for (i = 0; !x && i < d.forms.length; i++) x = d.forms[i][n];
  for (i = 0; !x && d.layers && i < d.layers.length; i++)
    x = MM_findObj(n, d.layers[i].document);
  if (!x && d.getElementById) x = d.getElementById(n);
  return x;
}
function MM_preloadImages() { //v3.0
  var d = document;
  if (d.images) {
    if (!d.MM_p) d.MM_p = new Array();
    var i, j = d.MM_p.length, a = MM_preloadImages.arguments;
    for (i = 0; i < a.length; i++)
      if (a[i].indexOf("#") != 0) { d.MM_p[j] = new Image; d.MM_p[j++].src = a[i]; }
  }
}
function MM_swapImgRestore() { //v3.0
  var i, x, a = document.MM_sr;
  for (i = 0; a && i < a.length && (x = a[i]) && x.oSrc; i++) x.src = x.oSrc;
}
function MM_swapImage() { //v3.0
  var i, j = 0, x, a = MM_swapImage.arguments;
  document.MM_sr = new Array;
  for (i = 0; i < (a.length - 2); i += 3)
    if ((x = MM_findObj(a[i])) != null) {
      document.MM_sr[j++] = x;
      if (!x.oSrc) x.oSrc = x.src;
      x.src = a[i + 2];
    }
}
</script>

<body onLoad="MM_preloadImages('https://xbawx.com/btn/assets/a6/a6123e8f5e63e584a09d69d3555770b2295cf761.gif')">
<a href="/"
   onMouseOver="MM_swapImage('home','','https://xbawx.com/btn/assets/a6/a6123e8f5e63e584a09d69d3555770b2295cf761.gif',1)"
   onMouseOut="MM_swapImgRestore()">
  <img name="home" width="88" height="31" border="0"
       src="https://xbawx.com/btn/assets/f8/f8fe94a7667d1041aee087e1c130f93c0b11462e.gif">
</a>
<p style="font:11px Verdana">Point at the button. That is four functions and about
forty lines of browser sniffing to change one image.</p>
sandboxed demo · breaks nothing but itselfrestart

The Free Host Injection

server side 1997 partly #

The advertisement your free host appended to your page after you uploaded it.

In 2026: The pattern outlived the hosts. GeoCities started with a watermark image in the corner, moved to a floating advert window, and Angelfire and Tripod did their own versions. You could not remove it, and a good deal of 90s JavaScript exists purely to fight it.

Where it came from: GeoCities, Angelfire and Tripod page footers, roughly 1997 to 2001. The exact markup varied by year and by host.
Wikipedia Archive Team

<p style="font:13px Verdana">Nothing visible happens here in 2026: the tracker
script is long dead and the popup is blocked because it opens on load. The
emptiness is the exhibit. Everything below was machinery, none of it yours.</p>

<!-- text below generated by server. PLEASE REMOVE -->
<!-- Counter/Statistics data collection code -->
<script language="JavaScript" src="http://us.js2.yimg.com/us.js.yimg.com/lib/smb/js/hosting/cp/js_source/whv2_001.js"></script>
<script language="javascript">
  geovisit();
</script>
<noscript><img src="http://visit.webhosting.yahoo.com/visit.gif?us1234567890" height="1" width="1"></noscript>

<!-- The floating advert window, opened on load -->
<script language="JavaScript">
  var ad = window.open("/adpopup.html", "ad",
    "width=250,height=250,toolbar=no,menubar=no,scrollbars=no");
</script>

<!-- And the script people pasted in to try to kill it -->
<script language="JavaScript">
  function killAd() { if (window.ad) { window.ad.close(); } }
  setTimeout(killAd, 1000);
</script>
sandboxed demo · breaks nothing but itselfrestart

BLOCKQUOTE As An Indent

layout hacks 1995 still works #

Quoting nothing, three deep, because it was the only way to move text to the right.

In 2026: It still works and it is still wrong. Nesting DL and DD for the same reason was equally common. Both put the wrong thing in the document outline, which matters to screen readers and to search engines.

Where it came from: Universal practice from about 1995. Called out as an abuse in the HTML 3.2 specification itself.
MDN

<!-- Indent one step -->
<blockquote>Not a quotation.</blockquote>

<!-- Indent three steps -->
<blockquote><blockquote><blockquote>
  Still not a quotation. Just further right.
</blockquote></blockquote></blockquote>

<!-- The other way people did it -->
<dl><dd><dl><dd>Also just an indent.</dd></dl></dd></dl>

<!-- And the third way -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Indented with non-breaking spaces.</p>

<!-- The 2026 version -->
<p style="margin-left: 4em">A margin.</p>
sandboxed demo · breaks nothing but itselfrestart

Layout By Non-Breaking Space

layout hacks 1996 still works #

Whitespace built out of entities, because HTML collapses real spaces.

In 2026: Works, and it is still the fastest way to break a layout on a phone. The empty table cell trick was necessary in Netscape 4, which drew no border or background at all around a genuinely empty cell.

Where it came from: Universal practice. The empty cell workaround is documented in Netscape's own authoring notes, 1996.
Wikipedia

<!-- Fake indentation -->
<p>&nbsp;&nbsp;&nbsp;&nbsp;This paragraph is indented four spaces.</p>

<!-- Fake column gap -->
<p>Left column&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Right column</p>

<!-- The empty cell that has to contain something -->
<table border="1" cellpadding="4">
<tr><td>Filled</td><td>&nbsp;</td></tr>
</table>

<!-- Fake vertical space -->
<br>&nbsp;<br>&nbsp;<br>

<!-- The 2026 version -->
<p style="text-indent:4em">Indented.</p>
<div style="height:2em"></div>
sandboxed demo · breaks nothing but itselfrestart

A FONT Tag In Every Cell

layout hacks 1996 still works #

Tables reset the font, so every cell needed its own copy of the same tag.

In 2026: It works, and it is why a 1998 page is four times the size it needs to be. Netscape 4 genuinely did not inherit font settings into table cells, so this was not laziness, it was the only thing that worked.

Where it came from: Netscape Navigator 4 inheritance behaviour, documented by frustrated authors everywhere from 1997.
MDN

<table border="0" cellpadding="4">
<tr>
  <td><font face="Arial, Helvetica, sans-serif" size="2" color="#000000">Name</font></td>
  <td><font face="Arial, Helvetica, sans-serif" size="2" color="#000000">Age</font></td>
</tr>
<tr>
  <td><font face="Arial, Helvetica, sans-serif" size="2" color="#000000">Sam</font></td>
  <td><font face="Arial, Helvetica, sans-serif" size="2" color="#000000">31</font></td>
</tr>
</table>

<!-- The 2026 version -->
<style>table { font: 14px Arial, Helvetica, sans-serif; color: #000 }</style>
sandboxed demo · breaks nothing but itselfrestart

The 216 Web Safe Colours

page decoration 1996 partly #

Six values per channel, because a lot of visitors were running 256 colour displays.

In 2026: Harmless now and entirely unnecessary. Any hex value where every pair is 00, 33, 66, 99, CC or FF survived the browser's dithering on an 8 bit screen. Anything else turned into speckles.

Where it came from: Lynda Weinman's browser-safe palette, published in Designing Web Graphics, 1996.
Wikipedia Wayback: lynda.com

<!-- Safe: every pair is 00 33 66 99 CC or FF -->
<font color="#FF0000">safe red</font>
<font color="#00CC66">safe green</font>
<font color="#3366CC">safe blue</font>

<!-- Not safe in 1996: dithered into speckles on a 256 colour screen -->
<font color="#FF3B2A">unsafe red</font>
<font color="#1E88E5">unsafe blue</font>

<script language="JavaScript">
// The whole cube, which every design book printed as a poster
var v = ["00","33","66","99","CC","FF"], out = "";
for (var r = 0; r < 6; r++)
  for (var g = 0; g < 6; g++)
    for (var b = 0; b < 6; b++)
      out += '<span style="display:inline-block;width:12px;height:12px;background:#'
           + v[r] + v[g] + v[b] + '"></span>';
document.write("<div style='width:216px;line-height:0'>" + out + "</div>");
</script>
sandboxed demo · breaks nothing but itselfrestart

document.write After Load

browser tricks 1997 partly #

Calling document.write once the page has finished loading wipes the page and starts a new one.

In 2026: Still true, and still a real bug people hit. During parsing document.write inserts at the current point. After load it implicitly calls document.open, which clears everything. The demo below destroys its own frame three seconds in.

Where it came from: Netscape JavaScript Guide, 1997, which warns about it in a note most people did not read.
MDN MDN document.open

<h2>This heading exists right now.</h2>
<p>Watch it disappear in three seconds.</p>

<script language="JavaScript">
setTimeout(function () {
  // After load, this implicitly calls document.open() first.
  document.write("<h2>Everything above is gone.</h2>");
  document.write("<p>document.write after load replaces the whole document.</p>");
  document.close();
}, 3000);
</script>
sandboxed demo · breaks nothing but itselfrestart

The javascript: URL

browser tricks 1996 partly #

Putting code in the href, which then has to return nothing or it replaces the page.

In 2026: Still works in an href and is a bad idea. If the expression returns a value, the browser navigates to it as a document, which is why every one of these ends in void(0) or a function with no return. Use an onclick handler, or a real button.

Where it came from: Netscape Navigator 2.0 JavaScript documentation, 1996.
Wikipedia HTML standard

<!-- The three forms you will find in old markup -->
<a href="javascript:void(0)" onclick="doThing()">Nothing in the href</a>
<a href="javascript:doThing()">Code in the href, relies on doThing returning nothing</a>
<a href="#" onclick="doThing(); return false">Hash plus return false</a>

<!-- What happens when the expression DOES return something. On a normal page
     the document becomes the word hello. This sandboxed frame refuses that
     replacement, so here the click does nothing at all. -->
<a href="javascript:'hello'">Click this and the page becomes the word hello</a>

<script language="JavaScript">
function doThing() { alert("clicked"); }
</script>

<!-- The 2026 version -->
<button type="button" onclick="doThing()">A button, which is what this always was</button>
sandboxed demo · breaks nothing but itselfrestart

Every MARQUEE Attribute

text effects 1996 partly #

The full set, including the ones that make it bounce, crawl, or stop when you point at it.

In 2026: All of it still renders in every browser, and the HTML standard keeps it that way: MARQUEE sits in the obsolete features section, banned for authors, required of browsers. It was Microsoft's answer to BLINK, which means the two worst tags of the era exist because of each other.

Where it came from: Microsoft Internet Explorer 2 HTML extensions, 1996. Documented in the HTML standard's obsolete features section as something browsers must still render.
MDN HTML standard

<marquee behavior="scroll" direction="left" scrollamount="6" scrolldelay="85"
         loop="infinite" width="100%" height="30" bgcolor="#000080"
         hspace="10" vspace="10"
         onmouseover="this.stop()" onmouseout="this.start()">
  <font color="#FFFF00" face="Comic Sans MS" size="4">
    *** POINT AT ME TO STOP *** SIGN MY GUESTBOOK ***
  </font>
</marquee>

<marquee behavior="alternate" scrollamount="4" width="60%">bounces off both ends</marquee>
<marquee behavior="slide" scrollamount="8" width="60%">slides in once and stops</marquee>
<marquee direction="up" height="60" scrollamount="2" width="200"
         style="border:1px solid #999">scrolls upward</marquee>
<marquee direction="right" scrollamount="3" width="60%">right to left, reversed</marquee>
sandboxed demo · breaks nothing but itselfrestart

cursor: hand

mouse and cursor 1997 dead #

Microsoft called the pointing hand cursor: hand, and half the web copied it before reading the spec.

In 2026: Dead. hand was never in any CSS specification, so every modern browser drops the declaration as invalid and the cursor stays an arrow. Pages that wrote both, hand first and pointer second, still work by accident, which is the only reason anyone got away with it for so long.

Where it came from: Microsoft Internet Explorer 4 CSS extensions, 1997. CSS2 chose pointer the following year, and IE quietly accepted both.
MDN

<style>
.hand    { cursor: hand; }     /* IE only, never standard */
.pointer { cursor: pointer; }  /* the CSS2 spelling */
</style>

<p class="hand">Hover me. cursor: hand. Nothing happens in 2026.</p>
<p class="pointer">Hover me. cursor: pointer. The hand you expected.</p>
sandboxed demo · breaks nothing but itselfrestart

The IE5/Mac Band Pass Filter

layout hacks 2000 dead #

A comment with a backslash in it, exploiting a parser bug to serve CSS to exactly one browser on one operating system.

In 2026: Dead in the most complete way possible: the browser it targeted is gone, and every current parser reads the hidden block as an ordinary comment, so the rules inside reach nobody at all. A filter with nothing left to pass.

Where it came from: Tantek Celik's IE5/Mac band pass filter, published on tantek.com, 2000.
Wikipedia tantek.com

<style>
p { color: black; }

/* Everything between these two comment tricks was visible
   ONLY to Internet Explorer 5 on the Macintosh */
/*\*//*/
p { color: red; font-weight: bold; }
/**/
</style>

<p>If this text is red, you are reading it on IE5 for Mac. It is not red.</p>
sandboxed demo · breaks nothing but itselfrestart

Aural Stylesheets

plugins and media 1998 dead #

CSS2 let you give your page a voice, a pitch, and a position in three dimensional space around the listener's head.

In 2026: Dead. No mainstream browser ever implemented aural stylesheets, and CSS 2.1 demoted the whole chapter to an appendix. Screen readers went their own way entirely. azimuth and elevation, which placed a paragraph's voice above and to the left of your head, remain the most ambitious CSS properties never to render anywhere.

Where it came from: CSS2 chapter 19, Aural style sheets, W3C 1998. Demoted to an informative appendix in CSS 2.1.
W3C

<style>
@media aural {
  h1     { voice-family: male; pitch: low; richness: 90; }
  p      { azimuth: center-left; elevation: above; }
  .shout { volume: x-loud; speak: spell-out; }
  .aside { play-during: url(harp.mid) repeat; }
}
</style>

<h1>This heading has a deep male voice.</h1>
<p>This paragraph is positioned to your upper left in 3D space.</p>
<p class="shout">THIS ONE IS SPELLED OUT AT FULL VOLUME.</p>
<p class="aside">This one has harp music playing under it.</p>
sandboxed demo · breaks nothing but itselfrestart

DHTML Behaviors and .htc Files

browser wars 1999 dead #

Internet Explorer let a stylesheet attach behaviour files to elements, mixing script into CSS on purpose.

In 2026: Dead. behavior: was IE only, ignored as an invalid property everywhere else, and IE10 removed it. The idea itself was not stupid, reusable components attached to markup came back years later as web components. The delivery mechanism, script loaded by a stylesheet, is exactly what Content Security Policy now exists to prevent.

Where it came from: Microsoft DHTML behaviors, Internet Explorer 5, MSDN, 1999.
Wikipedia

<style>
/* Attach a script file to elements FROM CSS */
li  { behavior: url(collapse.htc); }
div { behavior: url(fader.htc), url(dragdrop.htc); }
</style>

<!-- collapse.htc, an HTML Component file, looked like this: -->
<!--
<PUBLIC:COMPONENT>
  <PUBLIC:ATTACH EVENT="onclick" ONEVENT="toggle()" />
  <SCRIPT LANGUAGE="JScript">
    function toggle() { ... }
  </SCRIPT>
</PUBLIC:COMPONENT>
-->

<ul><li>In IE5 this list item collapsed on click. Here it is a list item.</li></ul>
sandboxed demo · breaks nothing but itselfrestart

-moz-binding and XBL

browser wars 2001 dead #

Mozilla's mirror of IE's behavior property: a CSS rule that bolted XML-defined script components onto elements.

In 2026: Dead. XBL never worked in any other browser, powered most of Firefox's own interface for fifteen years, and was cut off from the web in 2017 after too many security holes. Gecko finished deleting it internally in 2019. Its good ideas resurfaced as shadow DOM and custom elements.

Where it came from: Mozilla's XML Binding Language, in Gecko from 2001. It never ran anywhere else.
Wikipedia XBL analysis (Mozilla)

<style>
/* Mozilla's answer to IE's behavior: attach an XBL binding from CSS */
.fancy {
  -moz-binding: url(bindings.xml#roundedcorners);
}
</style>

<!-- bindings.xml held XML that mixed markup, script and style: -->
<!--
<binding id="roundedcorners">
  <content> ... anonymous content injected around the element ... </content>
  <implementation> ... methods and fields in JavaScript ... </implementation>
</binding>
-->

<div class="fancy">In 2001 Firefox this had script-built rounded corners.</div>
sandboxed demo · breaks nothing but itselfrestart

The Windows Theme In CSS

page decoration 1998 partly #

CSS2 shipped the entire Windows Appearance control panel as colour keywords, so a page could dress up as the visitor's desktop.

In 2026: Partly. The keywords still resolve, so the demo renders, but browsers now map them to fixed values instead of the user's actual theme, because reading the real desktop palette was a fingerprinting vector. CSS Color Level 3 deprecated the original list; a slimmed set came back for forced-colors accessibility modes. The button below will look grey and Windows-ish forever, no matter what your desktop does.

Where it came from: CSS2 system colors, W3C 1998, named directly after the Windows Appearance control panel.
MDN CSS2 spec

<style>
/* Paint your page with the visitor's actual Windows theme */
.win-button {
  background: ButtonFace;
  color: ButtonText;
  border: 2px outset ButtonHighlight;
  padding: 2px 12px;
}
.win-tip {
  background: InfoBackground;
  color: InfoText;
  border: 1px solid WindowFrame;
  padding: 2px 6px;
}
</style>

<p><span class="win-button">A Windows 98 button made of pure CSS</span></p>
<p><span class="win-tip">A tooltip painted in the visitor's own theme colours.</span></p>
sandboxed demo · breaks nothing but itselfrestart

The !important Double Declaration

layout hacks 2000 dead #

Old Internet Explorer ignored !important when the same property appeared twice, which turned a bug into a browser detector.

In 2026: Dead as a technique: every current browser honours !important, so both audiences now see red and the second declaration reaches nobody. It sits here as the purest specimen of the era's genre, CSS written for how browsers misbehaved rather than for what it said.

Where it came from: An Internet Explorer parser bug, catalogued on the CSS hack lists of the early 2000s.
Wikipedia

<style>
p.hack {
  color: red !important;   /* every correct browser stops here */
  color: blue;             /* IE4 and IE5 took this one instead */
}
</style>

<p class="hack">
  Red in a correct browser. Blue in old Internet Explorer.
  One property, two audiences, no scripting.
</p>
sandboxed demo · breaks nothing but itselfrestart

The Year 19100

time and date 1999 still works #

The JavaScript Y2K bug. getYear() returned 99 in 1999, so pages printed the century themselves. Then the year 100 arrived.

In 2026: Still works, which is the joke: getYear() survives in the compatibility annex of the JavaScript spec and still returns the year minus 1900, so the demo below glues 19 in front of that number and prints a four digit year wrong by a century. On the first of January 2000, half the web's footers read 19100. getFullYear() had already shipped two years earlier, in browsers nobody had upgraded.

Where it came from: JavaScript 1.0 Date.getYear, Netscape 2, 1996. The fix, getFullYear, shipped in 1998 and plenty of pages ignored it.
MDN Wikipedia: Y2K

<script language="JavaScript">
var today = new Date();
// getYear() returns the year minus 1900. In 1998 that was "98",
// so everyone just glued "19" in front of it.
document.write("Copyright 19" + today.getYear() + ". All rights reserved.");
</script>
sandboxed demo · breaks nothing but itselfrestart

showModalDialog

windows and alerts 1997 dead #

A popup that froze every script on the page until the visitor dealt with it, and then handed back a return value.

In 2026: Dead. Chrome removed it in 2014 and Firefox in 2017, because pausing the entire engine mid-function while a second page runs was a nightmare nobody wanted to maintain. The demo button now reports that showModalDialog is not a function. Its one honest descendant is the dialog element, which does the modal part without freezing the world.

Where it came from: Microsoft Internet Explorer 4, 1997. Removed from Chrome in 2014 and Firefox in 2017.
MDN

<script language="JavaScript">
function editDetails() {
  // Opens a dialog and STOPS EVERY SCRIPT until the visitor closes it,
  // then hands back whatever the dialog chose to return.
  var result = window.showModalDialog(
      "details.html", null,
      "dialogWidth:400px; dialogHeight:300px; status:no");
  alert("The dialog returned: " + result);
}
</script>

<button onclick="try { editDetails() } catch (e) {
  document.getElementById('out').innerHTML = e; }">Edit details</button>
<p id="out"></p>
sandboxed demo · breaks nothing but itselfrestart

Strings That Blink

text effects 1996 partly #

Strings in JavaScript have always known how to blink, shout and colour themselves. These methods are still in the language.

In 2026: Partly. Every one of these methods still exists, protected by Annex B of the ECMAScript specification, and still returns the markup it always did. fontcolor and link render fine. blink builds a perfectly good BLINK element that no browser has animated since 2013. A living fossil you can call from the console right now.

Where it came from: JavaScript 1.0, Netscape 2, 1996. Kept alive today by Annex B of the ECMAScript specification.
MDN

<script language="JavaScript">
// Every string knows how to wrap itself in a tag. This is real JavaScript.
document.write("Attention!".blink());
document.write("<br>");
document.write("Big red news".big().fontcolor("red"));
document.write("<br>");
document.write("Tiny teletype print".small().fixed());
document.write("<br>");
document.write("A link from a method".link("http://www.example.com/"));

// What blink() actually builds:
document.write("<xmp>" + "Attention!".blink() + "</xmp>");
</script>
sandboxed demo · breaks nothing but itselfrestart

The Earthquake Script

windows and alerts 1998 dead #

Scripts could grab the visitor's browser window and shake it around the screen, so of course everyone did.

In 2026: Dead. Browsers stopped honouring moveTo and moveBy for anything but small single-tab popups the script itself opened, because a page rearranging your desktop stopped being funny around the tenth ad that did it. The calls succeed silently and nothing moves.

Where it came from: Netscape JavaScript 1.2 window positioning, 1997. A staple of the free script archives as a welcome effect.
MDN

<script language="JavaScript">
// Shake the visitor's ENTIRE BROWSER WINDOW. A greeting, at the time.
function earthquake() {
  for (var i = 0; i < 10; i++) {
    window.moveBy(12, 0);  window.moveBy(0, 12);
    window.moveBy(-12, 0); window.moveBy(0, -12);
  }
  document.getElementById("r").innerHTML =
      "Forty moveBy calls issued. Did anything move?";
}
</script>

<button onclick="earthquake()">Welcome, visitor!</button>
<p id="r"></p>
sandboxed demo · breaks nothing but itselfrestart

The Pop-Under

windows and alerts 1999 dead #

Open the ad, shove it behind the window, and let the visitor discover it after you are long gone.

In 2026: Dead twice over. Popup blockers kill the open call unless it comes from a click, and even when one is allowed, blur and focus no longer reorder windows, so the ad lands on top like an honest popup. The whole genre of deniable advertising rested on two lines browsers now ignore.

Where it came from: Perfected by the ad networks around 1999, litigated over for a decade afterwards.
Wikipedia

<script language="JavaScript">
// The pop-UNDER: open an ad, then immediately hide it behind yourself.
// The visitor finds it hours later, long after they can blame you.
var ad = window.open("sponsor.html", "ad",
                     "width=400,height=300,left=50,top=50");
if (ad) {
  ad.blur();          // push the ad away
  window.focus();     // pull yourself back on top
  document.write("<p>Ad opened and buried.</p>");
} else {
  document.write("<p>The browser refused to open it at all.</p>");
}
</script>
sandboxed demo · breaks nothing but itselfrestart

The Chromeless Window

windows and alerts 1998 partly #

window.open took a shopping list of chrome to remove, up to and including the entire screen.

In 2026: Partly. The window still opens on a click, but almost the whole feature string is now ignored: the address bar is permanently welded on so a page cannot impersonate your bank, fullscreen=yes does nothing, and channelmode has been meaningless since IE4's channel bar died. You get a plain small window and you will like it.

Where it came from: Netscape and Microsoft window.open feature strings, 1996 onward. fullscreen= arrived with Internet Explorer 5.
MDN

<script language="JavaScript">
function kiosk() {
  // Strip the toolbar, menus, address bar, status bar, scrollbars,
  // or just take the whole screen with no chrome at all.
  window.open("art.html", "kiosk",
      "fullscreen=yes,channelmode=yes,toolbar=no,menubar=no," +
      "location=no,status=no,scrollbars=no,resizable=no");
}
</script>

<button onclick="kiosk()">Enter my site (best experienced fullscreen)</button>
sandboxed demo · breaks nothing but itselfrestart

The Birth of Ajax

browser wars 1999 partly #

Before fetch, before Ajax had a name, loading data meant asking Windows for an ActiveX control and hoping.

In 2026: Partly, in the funniest way: the two ActiveX rungs throw on every browser on earth, then the ladder lands on XMLHttpRequest, which the whole world implemented by copying Microsoft's control. The pattern still runs because its fallback became the standard. ActiveXObject itself died with Internet Explorer.

Where it came from: Microsoft.XMLHTTP shipped with Internet Explorer 5 in 1999, built for Outlook Web Access. Mozilla cloned it as XMLHttpRequest in 2000 and Ajax got its name in 2005.
MDN Wikipedia XHR Wayback: Ajax essay

<script language="JavaScript">
// Loading data without a page refresh, 1999 style. This ladder of
// disappointment is the actual birth certificate of Ajax.
function makeRequest() {
  try { return new ActiveXObject("Msxml2.XMLHTTP"); }    catch (e) {}
  try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
  if (window.XMLHttpRequest) return new XMLHttpRequest();
  return null;
}
var req = makeRequest();
document.write(req
  ? "<p>Got a request object: " + req + "</p>"
  : "<p>No way to fetch anything. Enjoy the full page reload.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

captureEvents and the Bitmask

browser wars 1997 dead #

In Netscape 4 events were a subscription service: no captureEvents call with the right bitmask, no mousemove for you.

In 2026: Dead as a mechanism, undead as a stub. The rival IE model needed no registration at all and the DOM standard followed suit, but the stub refused to die: the HTML standard now requires document.captureEvents to exist and do nothing, purely so scripts like this cannot throw. The demo finds it alive and useless. Every old mouse trail script starts with this incantation, which is why so many of them begin with if (document.layers).

Where it came from: Netscape Navigator 4 event model, JavaScript 1.2, 1997.
MDN HTML standard

<script language="JavaScript">
// Netscape 4: a page had to ASK to see events, with a bitmask,
// before any of its handlers would fire.
if (document.captureEvents) {
  document.captureEvents(Event.MOUSEMOVE | Event.CLICK);
}
document.onmousemove = function (e) {
  window.status = "Mouse at " + e.pageX + "," + e.pageY;
};
document.write(document.captureEvents
  ? "<p>captureEvents still exists, and the spec requires it to do nothing.</p>"
  : "<p>captureEvents is gone. Handlers just work now.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

enablePrivilege

browser wars 1997 dead #

Netscape's signed scripts could request superpowers by name, up to reading the visitor's hard drive, guarded by one dialog box.

In 2026: Dead. The privilege names were grand, UniversalFileRead, UniversalBrowserWrite, UniversalXPConnect, and the security model was a single Yes button. Firefox removed the whole mechanism in 2012. The idea of a page asking for scoped powers came back properly as permission prompts, which at least stopped offering the universe.

Where it came from: Netscape 4 signed script security model, 1997. Removed from Firefox 17 in 2012.
Wikipedia

<script language="JavaScript">
// A web page asking politely for the keys to your filesystem.
function readLocalFile() {
  netscape.security.PrivilegeManager
          .enablePrivilege("UniversalFileRead");
  // If the visitor clicked Yes on the warning box, the page
  // could now open files straight off their hard drive.
}
try { readLocalFile(); }
catch (e) { document.write("<p>Refused: " + e + "</p>"); }
</script>
sandboxed demo · breaks nothing but itselfrestart

Do You Have Flash?

plugins and media 1998 dead #

Every Flash site opened by interrogating navigator.plugins, deciding whether to show the intro or the download nag.

In 2026: Dead as a detector. navigator.plugins still exists but since 2021 it returns the same hardcoded list of five PDF viewer entries for everyone, because the real list was a fingerprinting goldmine. The demo prints what your browser admits to, which is what every other browser admits to. Flash itself ended in 2020.

Where it came from: Netscape JavaScript 1.1 navigator.plugins, 1996. The nag-or-intro pattern was every Flash site's front door.
MDN

<script language="JavaScript">
// Does the visitor have Flash? Walk the plugin list and see.
var hasFlash = false;
for (var i = 0; i < navigator.plugins.length; i++) {
  if (navigator.plugins[i].name.indexOf("Shockwave Flash") != -1)
    hasFlash = true;
}
document.write(hasFlash
  ? "<p>Flash detected. Loading intro...</p>"
  : "<p>No Flash. <a href='#'>Click here to download the plugin.</a></p>");

// What does the browser admit to in 2026?
document.write("<ul>");
for (var j = 0; j < navigator.plugins.length; j++)
  document.write("<li>" + navigator.plugins[j].name + "</li>");
document.write("</ul>");
</script>
sandboxed demo · breaks nothing but itselfrestart

The with Statement

browser tricks 1996 partly #

with dumped an object's every property into scope, saving keystrokes and destroying certainty about what any name meant.

In 2026: Partly. It still runs in a plain script tag, as the demo proves, but strict mode and modules ban it outright, so it cannot survive contact with modern code. Engines hate it because one with block turns every variable lookup into a guessing game. The keyword remains reserved forever, a monument to convenience.

Where it came from: JavaScript 1.0, Netscape 2, 1996. Banned by strict mode in ECMAScript 5, 2009.
MDN

<script language="JavaScript">
// Why type document.form1 five times when you can just... not?
with (document) {
  write("<p>Written from inside a with block.</p>");
}

with (Math) {
  document.write("<p>Circle area, radius 5: " + round(PI * pow(5, 2)) + "</p>");
}
</script>
sandboxed demo · breaks nothing but itselfrestart

setTimeout With A String

browser tricks 1996 partly #

setTimeout originally took a string of source code, which it compiled and ran when the timer fired. eval with a fuse.

In 2026: Partly. The string form is still valid and still fires on a page with no Content Security Policy. But a CSP without unsafe-eval blocks it exactly like eval, and this page sets one, so the demo fires the function form and shows the string form as text. Function arguments arrived in JavaScript 1.2 and won completely.

Where it came from: JavaScript 1.0 setTimeout, Netscape 2, 1996, which accepted only strings. Function arguments came with JavaScript 1.2.
MDN

<p id="g">Wait for it...</p>
<p id="note"></p>

<script language="JavaScript">
var visitor = "friend";
// The function form fires normally, a moment after load.
setTimeout(function () {
  document.getElementById("g").innerHTML =
      "Hello, " + visitor + "! Fired a second later, from a function.";
}, 1200);

// The original string form,  setTimeout("greet()", 1200),  is still valid
// JavaScript. A Content Security Policy with no 'unsafe-eval' blocks it like
// eval, so this page cannot fire it.
document.getElementById("note").innerHTML =
    "The STRING form runs on any page without a Content Security Policy. " +
    "This page has one, so the string form is blocked, the same as eval.";
</script>
sandboxed demo · breaks nothing but itselfrestart

The Other Event Model

browser wars 1997 partly #

Internet Explorer's whole rival event system: attachEvent to listen, a global window.event, srcElement, and returnValue = false.

In 2026: Partly, and the surviving parts are a scandal. attachEvent is long gone, IE11 dropped it in 2013. But window.event, srcElement and returnValue all still work in Chromium in 2026, kept alive because too many old handlers would break, so the demo's handler reads the global and succeeds. Firefox held out against window.event until 2018, then gave in.

Where it came from: Microsoft Internet Explorer 4 event model, 1997, the other half of the browser war that addEventListener settled.
MDN MDN window.event

<button id="box">Click me</button>
<p id="out"></p>

<script language="JavaScript">
function handler() {
  var e = window.event;      // the global. No argument needed!
  document.getElementById("out").innerHTML =
      "window.event still works. srcElement says: " + e.srcElement.tagName;
  e.returnValue = false;     // IE for preventDefault
}
var box = document.getElementById("box");
if (box.attachEvent) box.attachEvent("onclick", handler);  // the IE way
else box.onclick = handler;                                // everyone else
</script>
sandboxed demo · breaks nothing but itselfrestart

Reading Your History With CSS

browser tricks 2002 dead #

Style visited links purple, read the colour back with JavaScript, and you have just read the visitor's browsing history.

In 2026: Dead, deliberately. Browsers have lied to this script since 2010: getComputedStyle always reports the unvisited colour, and :visited itself is restricted to colour changes only, so layout tricks cannot leak it either. Before the fix, demo pages cheerfully listed which banks and forums you had been to. One of the few entries here that was killed for working too well.

Where it came from: Filed against Mozilla as bug 57351 in 2000, exploited widely from about 2002, fixed across browsers starting in 2010.
MDN Bugzilla 57351 Mozilla Hacks

<style>
#probe a         { color: #0000ee; }  /* unvisited: blue  */
#probe a:visited { color: #551a8b; }  /* visited: purple  */
</style>

<div id="probe">
  <a href="http://www.google.com/">google</a>
  <a href="http://www.geocities.com/">geocities</a>
</div>
<ul id="report"></ul>

<script type="text/javascript">
// Read each link's colour back. Purple meant the visitor had been there.
var links = document.getElementById("probe").getElementsByTagName("a");
for (var i = 0; i < links.length; i++) {
  var col = getComputedStyle(links[i], null).color;
  document.getElementById("report").innerHTML +=
      "<li>" + links[i].innerHTML + " reports " + col +
      " (the browser says: never visited)</li>";
}
</script>
sandboxed demo · breaks nothing but itselfrestart

escape and unescape

browser tricks 1996 partly #

The original URL encoders. escape turned characters above Latin-1 into %uXXXX sequences that no other system on earth understood.

In 2026: Partly. Both functions still exist, Annex B again, and still work on plain ASCII. But escape predates UTF-8 URLs: anything beyond Latin-1 becomes %u sequences that were never standard, so servers, other languages and encodeURIComponent all refuse to decode them. Every %u2026 littering an old guestbook traces back to this pair.

Where it came from: JavaScript 1.0, Netscape 2, 1996. Superseded by encodeURIComponent in ECMAScript 3, 1999.
MDN ECMA-262 Annex B

<script language="JavaScript">
var s = "café & 100% niño";

document.write("<p>escape():            " + escape(s) + "</p>");
document.write("<p>encodeURIComponent(): " + encodeURIComponent(s) + "</p>");

// And the party trick nothing else can decode:
document.write("<p>escape('\\u65e5\\u672c\\u8a9e') gives " +
               escape("日本語") + "</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

designMode

browser tricks 2000 still works #

One assignment makes the whole page editable, headings, paragraphs and all. Microsoft built it for webmail and it refused to die.

In 2026: Still works, everywhere, perfectly. designMode and its per-element sibling contentEditable are the engine under every rich text box on the web, so browsers maintain them with real care. The rare entry here that grew up respectable. Type in the demo, it will let you.

Where it came from: Microsoft Internet Explorer 5.5 designMode, 2000. Standardised into HTML5 alongside contentEditable.
MDN

<h3>This page is now editable.</h3>
<p>Click anywhere and start typing. Rearrange this paragraph. Delete
   the heading. The page will not stop you, because the page is now
   a word processor.</p>
<p><i>(Nothing is saved. Restart the demo and it all comes back.)</i></p>

<script language="JavaScript">
// One line turns the entire document into a text editor.
document.designMode = "on";
</script>
sandboxed demo · breaks nothing but itselfrestart

JavaScript Style Sheets

browser wars 1996 dead #

Netscape shipped a rival to CSS itself, written in JavaScript. This is the original CSS-in-JS, from 1996.

In 2026: Dead. JSSS ran only in Netscape 4, and Netscape 6 dropped it in 2000. Every browser since reads a style block typed text/javascript as an unknown script language and runs none of it, so the heading below stays its default size. The tag names had to be fully capitalised, document.tags.H1, or it threw.

Where it came from: Netscape JavaScript Style Sheets, Navigator 4, 1996. Dropped in Netscape 6, 2000. No other browser ever implemented it.
Wikipedia W3C JSSS submission

<!-- Netscape's answer to CSS: a stylesheet written in JavaScript -->
<style type="text/javascript">
  tags.H1.color = "purple";
  tags.H1.fontSize = "28pt";
  tags.P.fontStyle = "italic";
  document.classes.warn.all.color = "red";
</style>

<h1>Styled by JavaScript in 1996. Plain by 2026.</h1>
<p>This paragraph asked to be italic.</p>
<p class="warn">This one asked to be red.</p>
sandboxed demo · breaks nothing but itselfrestart

Media Types That Match Nothing

layout hacks 1998 dead #

CSS once had a media type for televisions, one for projectors, and one for handheld phones. The spec now orders browsers to ignore them.

In 2026: Dead. Media Queries 4 kept tv, projection, handheld and tty as recognised words but says each must evaluate to nothing, so the projection and handheld rules below never apply and the screen rule always wins. Opera really had a projection mode for slideshows, and early Nokia phones really requested the handheld sheet.

Where it came from: CSS2 media types, W3C 1998. Deprecated by Media Queries Level 4, which requires them to be recognised but never to match.
MDN Media Queries 4

<style>
/* Only ONE of these can win in 2026, and it is always the same one */
@media screen     { p { color: green; } }
@media projection { p { color: red;   font-weight: bold; } }
@media handheld   { p { color: blue;  } }
@media tv         { p { letter-spacing: 4px; } }
</style>

<p>Green, because screen matched. The other three types
   match nothing at all now, by order of the specification.</p>
sandboxed demo · breaks nothing but itselfrestart

The Opacity Four-Pack

layout hacks 2003 partly #

Before one opacity property worked everywhere, every faded box carried the real opacity line plus four legacy fallbacks and hoped one of them stuck.

In 2026: Partly. Exactly one line does the work now: opacity. The Mozilla, KHTML and Internet Explorer alpha() versions all parse to nothing, so the box below is forty percent opaque by the strength of a single modern property standing where four used to crowd. The IE filter line is why so many old pages had jagged text on faded elements.

Where it came from: The cross-browser opacity incantation of roughly 2003. Only the unprefixed property, standardised in CSS Color 3, still functions.
MDN CSS Color 3

<style>
.ghost {
  opacity: 0.4;                                 /* the standard, and the survivor */
  -moz-opacity: 0.4;                            /* Netscape 6 / early Firefox      */
  -khtml-opacity: 0.4;                          /* Safari's Konqueror-era engine   */
  filter: alpha(opacity=40);                    /* Internet Explorer 4 to 8        */
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; /* IE8 */
  background: #cc0000; color: #fff; padding: 20px;
}
</style>

<div class="ghost">Forty percent opaque. One line of five is doing it.</div>
sandboxed demo · breaks nothing but itselfrestart

Web Fonts, First Attempt

plugins and media 1997 dead #

Downloadable web fonts shipped in Internet Explorer 4 in 1997, failed, and came back as a standard twelve years later.

In 2026: Dead. Internet Explorer wanted .eot files, made by a clumsy Microsoft tool called WEFT, and no other browser ever read the format. The @font-face rule below asks for an .eot that no engine will load, so the heading falls back to a system font. The same rule, pointed at a .woff2, is how every custom font on the web arrives today.

Where it came from: Microsoft Embedded OpenType and the WEFT tool, Internet Explorer 4, 1997. @font-face returned to the web with WOFF around 2009.
Wikipedia W3C EOT submission

<style>
/* Internet Explorer 4, 1997. The .eot format died; the @font-face rule lived. */
@font-face {
  font-family: "Fancy";
  src: url(fancyfont.eot);                       /* IE-only Embedded OpenType */
  src: url(fancyfont.eot?#iefix) format("embedded-opentype");
}
h2 { font-family: "Fancy", Georgia, serif; }
</style>

<h2>This heading wanted a downloaded font in 1997.</h2>
<p>The .eot never loads, so you are reading the fallback.</p>
sandboxed demo · breaks nothing but itselfrestart

Rounded Corners The Hard Way

layout hacks 2001 still works #

Before border-radius, a rounded box meant four corner images and a nest of divs, and every design shop had its own tortured version.

In 2026: Still works perfectly, which is the joke: this box has genuinely rounded corners in 2026, built from four quarter-circle GIFs and no CSS radius at all. A whole cottage industry of generators and nine-slice table variants existed to spare people writing this by hand. One CSS property retired all of it in 2010.

Where it came from: Universal practice from about 2001. Dan Cederholm's Mountaintop Corners, A List Apart 2004, and the Nifty Corners generators were the most-copied versions. border-radius ended it around 2010.
MDN A List Apart

<style>
.rbox { position: relative; background: #dde6f0; padding: 22px;
        color: #14284a; font-family: Verdana, sans-serif; }
.rbox img { position: absolute; width: 16px; height: 16px; border: 0; }
.rc-tl { top: 0; left: 0 }    .rc-tr { top: 0; right: 0 }
.rc-bl { bottom: 0; left: 0 } .rc-br { bottom: 0; right: 0 }
</style>

<div class="rbox">
  <img class="rc-tl" src="https://xbawx.com/inc/img/demo/corner_tl.gif">
  <img class="rc-tr" src="https://xbawx.com/inc/img/demo/corner_tr.gif">
  <img class="rc-bl" src="https://xbawx.com/inc/img/demo/corner_bl.gif">
  <img class="rc-br" src="https://xbawx.com/inc/img/demo/corner_br.gif">
  Rounded corners, no border-radius. Four GIFs holding this shape together.
</div>
sandboxed demo · breaks nothing but itselfrestart

text-indent Minus 9999

layout hacks 2003 still works #

To show a logo but keep the real text for search engines, you printed the words and then shoved them nine thousand pixels off the left of the screen.

In 2026: Still works. The heading below carries real text, a background image, and text-indent: -9999px, so a visitor sees the graphic and a screen reader or search engine still reads the words. Whether that was clever or a dirty trick started a standards argument that ran for years. Modern code uses a clip instead, but this still renders exactly as intended.

Where it came from: The Phark image replacement method, Mike Rundle, 2003, refining the earlier Fahrner technique. Debated on webdesign lists for years.
MDN CSS-Tricks museum

<style>
h2.logo {
  width: 360px; height: 60px;
  background: url(https://xbawx.com/inc/img/demo/header_mysite.png) no-repeat;
  text-indent: -9999px;      /* the words leave the screen, the meaning stays */
  overflow: hidden;
  margin: 0;
}
</style>

<h2 class="logo">My Cool Site</h2>
<p>You see a graphic. A search engine still reads the words "My Cool Site".</p>
sandboxed demo · breaks nothing but itselfrestart

XML Data Islands

browser wars 1999 dead #

Internet Explorer let you paste an XML document inside the page and bind its rows straight into an HTML table with two attributes.

In 2026: Dead. datasrc and datafld were Internet Explorer only, and the binding stopped working in IE10's standards mode. Every current browser renders the inline XML as naked text and leaves the bound table empty, so you can see the whole ambition collapse in one screen: the data is right there and nothing reads it.

Where it came from: Microsoft XML Data Islands, Internet Explorer 5, MSDN 1999. The binding died in Internet Explorer 10's standards mode.
Wikipedia

<!-- Inline XML, then bind its fields into a table. Internet Explorer only. -->
<xml id="cds">
  <catalog>
    <cd><title>Homework</title><artist>Daft Punk</artist></cd>
    <cd><title>Dummy</title><artist>Portishead</artist></cd>
  </catalog>
</xml>

<table border="1" datasrc="#cds">
  <thead><tr><th>Title</th><th>Artist</th></tr></thead>
  <tr>
    <td><span datafld="title"></span></td>
    <td><span datafld="artist"></span></td>
  </tr>
</table>

<p>In IE5 this table filled itself from the XML above. Here it stays empty.</p>
sandboxed demo · breaks nothing but itselfrestart

The Web Page As Desktop App

browser wars 1999 dead #

One tag in the head turned a web page into a trusted Windows program with full read and write access to your files and registry.

In 2026: Dead in the browser, undead on the disk. An .hta file opened by mshta.exe ran outside all web security, which is exactly why malware loves the format and why mshta.exe still ships in Windows 11. In a browser the hta:application tag is just unknown markup, so the page below renders as an ordinary paragraph with none of the powers it declares.

Where it came from: Microsoft HTML Applications, Internet Explorer 5, 1999. mshta.exe still ships in Windows 11.
Wikipedia

<html>
<head>
  <title>My Utility</title>
  <!-- This one tag, in a .hta file, meant NO web sandbox at all -->
  <hta:application
      id="app"
      applicationname="MyUtility"
      border="thin"
      scroll="no"
      singleinstance="yes" />
</head>
<body>
  <p>Saved as .hta and double-clicked, this had your whole filesystem.
     Loaded in a browser, it is a paragraph.</p>
</body>
</html>
sandboxed demo · breaks nothing but itselfrestart

The WebTV Dialect

dead tags 1996 dead #

The set-top box that put the web on televisions had its own HTML tags, including one that drew a live waveform of the sound it was playing.

In 2026: Dead. WebTV, later MSN TV, rendered on one appliance and invented markup to match it: audioscope drew an oscilloscope of the current audio, blackface set double-weight bold, and shadow drew drop-shadowed text. No desktop browser ever knew any of them, so all three sit inert below, a whole dialect for a machine almost nobody kept.

Where it came from: WebTV proprietary HTML extensions, WebTV Networks, 1996 onward, later MSN TV. Documented in the WebTV HTML reference.
Wikipedia Wayback: WebTV dev

<!-- Tags that existed only on the WebTV set-top box -->

<!-- A live oscilloscope of the playing audio -->
<audioscope width="200" height="80" leftcolor="#00ff00" rightcolor="#ff0000">

<!-- Double-weight bold -->
<blackface>This was extra bold on a television in 1997.</blackface>

<!-- Drop-shadowed heading -->
<shadow><h2>Shadowed Heading</h2></shadow>

<p>On a desktop browser, none of the three tags above does anything.</p>
sandboxed demo · breaks nothing but itselfrestart

Page Transitions

meta and head 1997 dead #

Internet Explorer could wipe, dissolve or checkerboard from one page to the next, set by a meta tag, and the hobby web went wild with it.

In 2026: Dead. The Page-Enter and Page-Exit meta tags with revealTrans were an Internet Explorer feature no other browser adopted, and modern Edge ignores them too. The tags below are inert, so the page just appears. Transition number 23 was random, which meant a lot of 1998 homepages greeted you with a different wipe every time.

Where it came from: Microsoft DirectAnimation revealTrans page transitions, Internet Explorer 4, 1997.
Wikipedia

<!-- Wipe INTO this page over two seconds, transition 23 (random) -->
<meta http-equiv="Page-Enter" content="revealTrans(Duration=2.0,Transition=23)">

<!-- And dissolve OUT when leaving it -->
<meta http-equiv="Page-Exit" content="revealTrans(Duration=1.5,Transition=12)">

<!-- transitions ran 0 to 23: box in, box out, circle, wipes,
     blinds, checkerboards, random dissolve, and the random pick -->

<h2>This page arrived with no animation at all.</h2>
sandboxed demo · breaks nothing but itselfrestart

Server-Side JavaScript, 1996

server side 1996 dead #

Netscape ran JavaScript on the server a decade before Node, marked off with a SERVER tag right inside the page.

In 2026: Dead, with a sting. LiveWire compiled the SERVER blocks on Netscape Enterprise Server and sent only their output to the browser. A modern browser has no server step, so it treats SERVER as an unknown element and prints the code inside as page text, which is precisely the leak that exposed database passwords on every misconfigured LiveWire site.

Where it came from: Netscape LiveWire server-side JavaScript, Netscape Enterprise Server, 1996.
Wikipedia

<h2>Welcome</h2>

<!-- On a Netscape LiveWire server this ran and vanished, leaving output -->
<server>
  var visits = project.lock() ? ++project.hits : 0;
  project.unlock();
  write("You are visitor number " + visits + ".");
  // database.SQLTable("SELECT * FROM users WHERE pass = '" + secret + "'");
</server>

<p>In a browser with no server, the code above leaks out as plain text.
   That is exactly how LiveWire sites spilled their secrets.</p>
sandboxed demo · breaks nothing but itselfrestart

The Page That Read Your Clipboard

browser tricks 1999 dead #

Internet Explorer let any web page read whatever you had copied, silently, with no permission and no prompt.

In 2026: Dead, thankfully. window.clipboardData.getData was on by default in Internet Explorer 5, so a page could poll your clipboard on a timer and post home whatever passwords or addresses you had copied. Bugtraq documented the theft around 2002. Every modern browser returns undefined for the object, as the demo shows, and the real Clipboard API now demands permission and a gesture.

Where it came from: Microsoft window.clipboardData, Internet Explorer 5, 1999. Reported as a silent-read privacy hole on Bugtraq around 2002.
MDN

<button onclick="peek()">What have you copied?</button>
<p id="out"></p>

<script language="JavaScript">
function peek() {
  var out = document.getElementById("out");
  // Internet Explorer 5 answered this with your actual clipboard. No prompt.
  if (window.clipboardData) {
    out.innerHTML = "Your clipboard says: " + window.clipboardData.getData("Text");
  } else {
    out.innerHTML = "window.clipboardData is undefined. The hole is closed.";
  }
}
</script>
sandboxed demo · breaks nothing but itselfrestart

The Truly Chromeless Popup

windows and alerts 2000 dead #

Internet Explorer had a popup with no title bar, no border and no frame at all, a bare rectangle a script could draw anywhere on screen.

In 2026: Dead. window.createPopup gave a window with zero chrome, which made it perfect for real menus and equally perfect for faking a Windows dialog or a browser warning over any site. No other browser implemented it, and it died with Internet Explorer. The demo reports that createPopup is not a function.

Where it came from: Microsoft window.createPopup, Internet Explorer 5.5, 2000. Implemented by no other browser.
MDN

<button onclick="show()">Open a chromeless popup</button>
<p id="out"></p>

<script language="JavaScript">
function show() {
  try {
    // A popup with NO title bar and NO border. Draw it anywhere.
    var pop = window.createPopup();
    var body = pop.document.body;
    body.style.border = "1px solid black";
    body.style.background = "#ffffe1";
    body.innerHTML = "A borderless box. Could be a menu. Could be a fake alert.";
    pop.show(100, 100, 260, 40, document.body);
  } catch (e) {
    document.getElementById("out").innerHTML = "createPopup is gone: " + e;
  }
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Relaxing The Same Origin

browser tricks 1996 dead #

Two pages from different subdomains could agree to drop their guard and script each other by both setting document.domain.

In 2026: Dead by default. For 27 years, shop.example.com and www.example.com could both set document.domain to example.com and reach into each other's frames. Chrome 115 in 2023 made the setter a silent no-op under origin-keyed agent clustering, so the assignment below runs, changes nothing, and the old cross-subdomain trick is over. postMessage is the sanctioned replacement.

Where it came from: Netscape JavaScript document.domain, 1996. Neutralised by Chrome 115's origin-keyed agent clusters, 2023.
MDN

<script language="JavaScript">
var before = document.domain;
// The 1996 handshake: both pages set this to the shared parent domain,
// and the same-origin wall between subdomains came down.
try { document.domain = document.domain.split(".").slice(-2).join("."); }
catch (e) {}
document.write("<p>document.domain before: " + before + "</p>");
document.write("<p>after the assignment:  " + document.domain + "</p>");
document.write("<p>In 2026 the setter is a silent no-op. The wall stays up.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Calling Java From The Page

plugins and media 1996 dead #

Netscape let page JavaScript reach straight into Java, calling System.out and opening AWT windows as if the applet runtime were part of the language.

In 2026: Dead. LiveConnect bridged JavaScript and the Java plugin both ways, so a script could write java.lang.System.out.println or pop a java.awt.Frame. Browsers removed plugin Java around 2015 and the whole bridge went with it. The demo finds no java object and says so. For a while, though, the browser really did contain a second entire language.

Where it came from: Netscape LiveConnect, JavaScript to Java bridge, Navigator 3, 1996. Removed with plugin Java around 2015.
Wikipedia

<button onclick="callJava()">Print from Java</button>
<p id="out"></p>

<script language="JavaScript">
function callJava() {
  var out = document.getElementById("out");
  try {
    // Reach out of JavaScript and into the Java runtime, 1996 style.
    java.lang.System.out.println("Hello from Java, via the page.");
    var v = java.lang.System.getProperty("java.version");
    out.innerHTML = "Java answered. Version: " + v;
  } catch (e) {
    out.innerHTML = "No java object. LiveConnect and the plugin are gone: " + e;
  }
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Storage Before localStorage

browser tricks 1999 partly #

Before the browser had real storage, people smuggled data across pages in the window's name and hid kilobytes in an Internet Explorer behavior.

In 2026: Partly. window.name survives navigation and still holds a string, so the demo writes to it and reads it back, a genuine relic that still runs. Its partner, Internet Explorer's userData behavior, saved 64KB to a hidden store years before localStorage arrived in Internet Explorer 8, and that half is long dead. Two workarounds for a gap the platform took a decade to close.

Where it came from: window.name as transport, and Microsoft's userData behavior in Internet Explorer 5, 1999. localStorage arrived with Internet Explorer 8, 2009.
MDN

<button onclick="stash()">Stash data in window.name</button>
<p id="out"></p>

<script language="JavaScript">
function stash() {
  // window.name persists across page loads and holds any string.
  // People used it as a data smuggler before sessionStorage existed.
  window.name = "cart=3;user=guest;ts=" + (new Date()).getTime();
  var msg = "window.name now holds: " + window.name;

  // The IE half: a hidden element that saved 64KB to disk. Long gone.
  var d = document.createElement("div");
  msg += (typeof d.addBehavior === "function")
    ? "<br>userData behavior available (old IE)."
    : "<br>userData behavior is gone. window.name still works.";
  document.getElementById("out").innerHTML = msg;
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Data Tainting

browser tricks 1996 dead #

Netscape 3 had a security model where data could be marked tainted, and one function survived returning false about it for the next twenty years.

In 2026: Dead. Tainting let a script mark values as sensitive so they could not leak across origins, an opt-in experiment in Navigator 3 that Navigator 4 abandoned. The taint and untaint functions vanished, but navigator.taintEnabled lingered in browsers for two decades, hardwired to return false, purely so ancient scripts would not crash. The demo calls that faithful little liar.

Where it came from: Netscape data tainting, Navigator 3, JavaScript 1.1, 1996. Abandoned in Navigator 4; the taintEnabled stub outlived it by two decades.
MDN

<script language="JavaScript">
// Netscape 3's data-tainting security check. It still answers, always the same.
if (navigator.taintEnabled) {
  document.write("<p>navigator.taintEnabled() returns: " +
                 navigator.taintEnabled() + "</p>");
  document.write("<p>It has returned false, unchanged, for about twenty years, " +
                 "so old scripts that call it do not throw.</p>");
} else {
  document.write("<p>Even the stub is gone now.</p>");
}
</script>
sandboxed demo · breaks nothing but itselfrestart

Mutation Events

browser tricks 2000 dead #

The first way to watch the page change fired an event for every node inserted or removed, and it was so slow it held the whole platform back.

In 2026: Dead, and only just. DOM Level 2 mutation events like DOMNodeInserted let a script react to the document editing itself, but firing synchronously on every change wrecked performance and blocked new browser features. Deprecated in 2011, replaced by MutationObserver, and finally removed in Chrome 127, Firefox 140 and Safari 26. The listener below is attached and, in 2026, never fires.

Where it came from: DOM Level 2 mutation events, W3C 2000. Deprecated 2011 for MutationObserver, removed from Chrome 127 in 2024.
MDN Chrome blog caniuse

<div id="host"><b>Watch this box.</b></div>
<p id="out">Listener attached. Waiting...</p>

<script language="JavaScript">
var host = document.getElementById("host");
var out = document.getElementById("out");

// The old way to observe the DOM: one event per change, fired synchronously.
host.addEventListener("DOMNodeInserted", function () {
  out.innerHTML = "DOMNodeInserted fired! (You are in an old browser.)";
});

// Now mutate the box. In 2026 the handler above never runs.
host.appendChild(document.createTextNode(" Changed at " + Date.now() + "."));
setTimeout(function () {
  if (out.innerHTML.indexOf("fired") === -1)
    out.innerHTML = "The box changed and the event never fired. Removed in 2024.";
}, 300);
</script>
sandboxed demo · breaks nothing but itselfrestart

The Opener Backdoor

browser tricks 1997 partly #

A link that opened a new tab handed the new page a live handle to the old one, enough to quietly replace it with a fake.

In 2026: Partly. A target=_blank page could read window.opener and set opener.location to a phishing copy of the page behind it, a trick called reverse tabnabbing. Browsers made target=_blank imply noopener from 2019 to 2021 (Safari 12.1, Firefox 79, Chrome 88), so anchors are safe now, but window.open still hands over a live opener. This sandboxed demo cannot navigate its opener, so it just reports what the handle looks like.

Where it came from: The window.opener relationship, Netscape 1996. Reverse tabnabbing named around 2016; implicit noopener shipped in Chrome 88, Firefox 79 and Safari 12.1, 2019 to 2021.
MDN OWASP

<button onclick="inspect()">Inspect window.opener</button>
<p id="out"></p>

<script language="JavaScript">
function inspect() {
  // The attack was: opener.location = "https://phish.example/login";
  // The opener page would silently become a fake while you read this one.
  var msg = "window.opener is: " + String(window.opener) + ".<br>";
  msg += "Anchors got implicit rel=noopener in 2021, which closed the door "
       + "for target=_blank links. window.open still leaves it ajar.";
  document.getElementById("out").innerHTML = msg;
}
</script>
sandboxed demo · breaks nothing but itselfrestart

The parseInt Octal Trap

browser tricks 1997 dead #

For years, a number with a leading zero was read as octal, so parseInt of 08 came back as zero and broke date forms every August.

In 2026: Dead, and the demo proves it. Old JavaScript treated a leading zero as an octal flag, so parseInt('08') and parseInt('09') returned 0 because 8 and 9 are not octal digits. Month and day fields full of 08 and 09 quietly failed all through August and September until ECMAScript 5 outlawed the guess in 2009. In 2026 every line below returns what you expect.

Where it came from: ECMAScript 3 octal parsing in parseInt, 1999 era. ECMAScript 5 removed the octal interpretation in 2009.
MDN

<script language="JavaScript">
// The trap: a leading zero once meant "read this as octal".
var tests = ["08", "09", "010", "0755"];
document.write("<ul>");
for (var i = 0; i < tests.length; i++) {
  document.write("<li>parseInt('" + tests[i] + "') = " +
                 parseInt(tests[i]) +
                 "  (safe form: parseInt('" + tests[i] + "', 10) = " +
                 parseInt(tests[i], 10) + ")</li>");
}
document.write("</ul>");
document.write("<p>In 1999 the first two returned 0 and August broke. Fixed in ES5.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

The Split Script Tag

browser tricks 1997 still works #

To write a script tag from inside a script, you had to break the closing tag in half, because the HTML parser could not tell code from markup.

In 2026: Still works, and still necessary, which is the joke. An HTML parser ends the current script the instant it sees the characters of a closing script tag, even inside a JavaScript string, so document.write of a script element must split the tag. Every ad tag, counter and analytics snippet of the era carried this, and it would still bite you today.

Where it came from: A consequence of HTML parsing, universal in inline scripts from the mid 1990s. Still required today.
MDN

<p id="out"></p>

<script language="JavaScript">
// You cannot write "</scr" + "ipt>" as one piece. The parser would
// see the closing tag inside your string and end THIS script early.
var slot = document.getElementById("out");
var tag = "<scr" + "ipt>document.title='written from a string'</scr" + "ipt>";
slot.innerHTML = "This string builds a script tag without ending the current one:" +
                 "<br><code>" + tag.replace(/</g, "&lt;") + "</code>";
</script>
sandboxed demo · breaks nothing but itselfrestart

Banana

luljs still works #

Spell the word banana out of string concatenation and one stray plus sign.

In 2026: Still works. The `+ +"a"` in the middle is a unary plus applied to the string "a", which coerces it to NaN. So the pieces join as "b" + "a" + NaN + "a", spelling baNaNa, and toLowerCase lowers it.

Where it came from: A coercion gag collected in the wtfjs list. It rests on the unary + operator turning a non-numeric string into NaN.
MDN wtfjs

<script>
var out = ("b" + "a" + + "a" + "a").toLowerCase();
document.write("<h2 style='font-family:sans-serif'>" + out + "</h2>");
document.write("<p><code>(\"b\" + \"a\" + + \"a\" + \"a\").toLowerCase()</code></p>");
document.write("<p>The <code>+ +\"a\"</code> is a unary plus on \"a\", which is <b>NaN</b>, so it spells <b>baNaNa</b>.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

0.1 + 0.2

luljs still works #

Add two plain decimals and get a number no pocket calculator would show.

In 2026: Still true in nearly every language, not just JavaScript, because they all use IEEE 754 doubles. 0.1 and 0.2 cannot be stored exactly in binary floating point, so their sum lands a hair above 0.3. That is why 0.1 + 0.2 === 0.3 is false, and why money should never be a float.

Where it came from: Not a JavaScript bug but IEEE 754 binary floating point, shared by almost every language. The site 0.30000000000000004.com catalogues the same result across dozens of them.
0.30000000000000004.com Goldberg paper

<script>
document.write("<p><code>0.1 + 0.2</code> is <b>" + (0.1 + 0.2) + "</b></p>");
document.write("<p><code>0.1 + 0.2 === 0.3</code> is <b>" + (0.1 + 0.2 === 0.3) + "</b></p>");
document.write("<p>Binary floating point cannot hold 0.1 or 0.2 exactly.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

NaN Is A Number

luljs still works #

Not-a-Number reports its own type as number, and it never equals itself.

In 2026: Still works. NaN is a value of the Number type by the IEEE 754 standard, so typeof NaN is "number". It is also the only value in the language not equal to itself: NaN === NaN is false. That is exactly how isNaN and Number.isNaN have to test for it.

Where it came from: Both facts come from IEEE 754: NaN is a number-typed value, and the standard defines it as unordered, so it compares unequal to everything, itself included.
Wikipedia MDN NaN

<script>
document.write("<p><code>typeof NaN</code> is <b>" + (typeof NaN) + "</b></p>");
document.write("<p><code>NaN === NaN</code> is <b>" + (NaN === NaN) + "</b></p>");
document.write("<p>NaN is number-typed, and the standard defines it as unequal to everything.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

typeof null

luljs still works #

The type of nothing is "object", a bug baked in during JavaScript's first ten days.

In 2026: Still works, and it will never be fixed. In the original 1995 engine, every value carried a type tag in its low bits, and the tag for objects was 000. null was the machine null pointer, all zero bits, so typeof null read as "object". Brendan Eich has called it a bug; a fix was proposed and rejected because too much code relies on it.

Where it came from: Traced to Brendan Eich's original ten-day implementation of JavaScript in 1995, where null's zero pointer collided with the object type tag. Alexander Ellis documents the bit-level history.
Alexander Ellis Wayback: 2ality

<script>
document.write("<p><code>typeof null</code> is <b>" + (typeof null) + "</b></p>");
document.write("<p><code>typeof undefined</code> is <b>" + (typeof undefined) + "</b></p>");
document.write("<p>null's all-zero bits matched the object type tag in the 1995 engine.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

[] == ![]

luljs still works #

An empty array is equal to the negation of itself.

In 2026: Still works. ![] is false, because any object is truthy so its negation is false. Then [] == false triggers coercion: the boolean becomes 0, the array becomes "" and then 0, and 0 == 0 is true.

Where it came from: A canonical entry in the wtfjs collection. It combines the truthiness of objects with the coercion rules of loose equality into one contradiction.
wtfjs MDN Equality

<script>
document.write("<p><code>![]</code> is <b>" + (![]) + "</b></p>");
document.write("<p><code>[] == ![]</code> is <b>" + ([] == ![]) + "</b></p>");
document.write("<p>Both sides collapse to 0 under == coercion.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

max() Is Less Than min()

luljs still works #

Called with no arguments, the maximum comes out smaller than the minimum.

In 2026: Still works, and it is correct. Math.max() with nothing to compare returns -Infinity, and Math.min() returns +Infinity, so max really is less than min. The identities are chosen so that taking the max or min of two joined lists still works, which forces the empty case to the opposite infinity.

Where it came from: Charlie Harvey worked out why the empty-argument identities must be the opposite infinities: it keeps max and min associative when you combine lists.
Charlie Harvey MDN Math.max

<script>
document.write("<p><code>Math.max()</code> is <b>" + Math.max() + "</b></p>");
document.write("<p><code>Math.min()</code> is <b>" + Math.min() + "</b></p>");
document.write("<p><code>Math.max() < Math.min()</code> is <b>" + (Math.max() < Math.min()) + "</b></p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

true + true

luljs still works #

Add booleans together and they turn into numbers.

In 2026: Still works. Under arithmetic +, a boolean coerces through ToNumber, so true becomes 1 and false becomes 0. That makes true + true equal 2 and true + true + true equal 3.

Where it came from: A direct result of the ToNumber coercion that the addition operator applies to booleans, documented on MDN's addition page.
MDN

<script>
document.write("<p><code>true + true</code> is <b>" + (true + true) + "</b></p>");
document.write("<p><code>true + true + true</code> is <b>" + (true + true + true) + "</b></p>");
document.write("<p><code>true * 5</code> is <b>" + (true * 5) + "</b></p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Plus Joins, Minus Subtracts

luljs still works #

Adding a string glues it on, subtracting a string does arithmetic.

In 2026: Still works. The + operator prefers string concatenation whenever either side is a string, so "11" + 1 is "111". Every other arithmetic operator has no string mode at all, so it coerces both sides to numbers: "11" - 1 is 10.

Where it came from: The split comes from + being the only arithmetic operator with a string mode; every other one forces numbers, as MDN's subtraction page spells out.
MDN

<script>
document.write("<p><code>\"11\" + 1</code> is <b>" + ("11" + 1) + "</b></p>");
document.write("<p><code>\"11\" - 1</code> is <b>" + ("11" - 1) + "</b></p>");
document.write("<p><code>\"5\" * \"3\"</code> is <b>" + ("5" * "3") + "</b></p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Sorting Numbers Wrong

luljs still works #

Sort a list of numbers and they come back in the wrong order.

In 2026: Still works, and it bites people constantly. Array.prototype.sort with no comparator converts every element to a string and sorts those, so "16" sorts before "2". You have to pass (a, b) => a - b to sort numbers as numbers.

Where it came from: The default comparator in Array.prototype.sort converts elements to strings before comparing them, as documented on MDN.
MDN

<script>
document.write("<p><code>[16, 8, 4, 2].sort()</code> is <b>" + JSON.stringify([16,8,4,2].sort()) + "</b></p>");
document.write("<p><code>[16, 8, 4, 2].sort((a,b)=>a-b)</code> is <b>" + JSON.stringify([16,8,4,2].sort(function(a,b){return a-b;})) + "</b></p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Non-Transitive ==

luljs still works #

A equals B and A equals C, but B does not equal C.

In 2026: Still works. 0 == "" is true and 0 == "0" is true, because == coerces the strings to numbers. But "" == "0" compares two strings directly, with no coercion, so it is false. Loose equality breaks the basic rule of transitivity, which is the whole reason === exists.

Where it came from: A textbook consequence of the == algorithm coercing across types but comparing same-type operands directly, laid out on MDN's equality page.
MDN

<script>
document.write("<p><code>0 == \"\"</code> is <b>" + (0 == "") + "</b></p>");
document.write("<p><code>0 == \"0\"</code> is <b>" + (0 == "0") + "</b></p>");
document.write("<p><code>\"\" == \"0\"</code> is <b>" + ("" == "0") + "</b></p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

The null Comparison Paradox

luljs still works #

null is not greater than zero, not equal to zero, yet null >= 0 is true.

In 2026: Still works. null == 0 is false, because == has a special rule that null equals only null and undefined. But null >= 0 uses the relational algorithm, which coerces null through ToNumber to 0, so 0 >= 0 is true. The two comparison families treat null by completely different rules.

Where it came from: == gives null its own special case while the relational operators coerce it to 0, so >= and == disagree. MDN's greater-than-or-equal page documents the numeric coercion.
MDN

<script>
document.write("<p><code>null == 0</code> is <b>" + (null == 0) + "</b></p>");
document.write("<p><code>null > 0</code> is <b>" + (null > 0) + "</b></p>");
document.write("<p><code>null >= 0</code> is <b>" + (null >= 0) + "</b></p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

Chained Comparisons Lie

luljs still works #

1 < 2 < 3 is true, but 3 > 2 > 1 is false.

In 2026: Still works. JavaScript has no chained comparison; it evaluates left to right. 1 < 2 is true, and true coerces to 1, so 1 < 3 is true. But 3 > 2 is also true, which is 1, and 1 > 1 is false.

Where it came from: JavaScript evaluates comparisons left to right with no chaining, coercing each boolean result to a number for the next, as MDN's less-than page describes.
MDN

<script>
document.write("<p><code>1 < 2 < 3</code> is <b>" + (1 < 2 < 3) + "</b></p>");
document.write("<p><code>3 > 2 > 1</code> is <b>" + (3 > 2 > 1) + "</b></p>");
document.write("<p>Each comparison returns a boolean that the next one treats as 1 or 0.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

console.log Returns Nothing

luljs still works #

Nest console.log inside itself and watch the undefineds stack up.

In 2026: Still works. console.log prints its arguments and returns undefined. So the innermost call prints "lol", and every call wrapped around it prints the undefined that the call inside it returned. Four logs, one real message, three undefineds. Open the browser console to see it.

Where it came from: console.log is defined to return undefined by the WHATWG console standard, so nesting it prints the return value of each inner call.
WHATWG

<script>
// Open the browser console to see the output stack up.
console.log(console.log(console.log(console.log("lol"))));
document.write("<p>Open the browser console. It prints:</p>");
document.write("<pre>lol\nundefined\nundefined\nundefined</pre>");
document.write("<p><code>console.log</code> returns <b>undefined</b>, which the call around it then logs.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

The Precision Cliff

luljs still works #

Count past a certain integer and the numbers start skipping.

In 2026: Still works. JavaScript numbers are 64-bit floats, so only integers up to Number.MAX_SAFE_INTEGER, about nine quadrillion, are exact. Past it the gaps between representable values grow wider than 1, so 9999999999999999 rounds to 10000000000000000 and x + 1 === x can be true.

Where it came from: A consequence of numbers being IEEE 754 doubles: integers stay exact only up to Number.MAX_SAFE_INTEGER, documented on MDN.
MDN

<script>
document.write("<p><code>9999999999999999</code> is <b>" + 9999999999999999 + "</b></p>");
document.write("<p><code>Number.MAX_SAFE_INTEGER</code> is <b>" + Number.MAX_SAFE_INTEGER + "</b></p>");
document.write("<p><code>MAX_SAFE_INTEGER + 1 === MAX_SAFE_INTEGER + 2</code> is <b>" + (Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2) + "</b></p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

NaN Can Hide

luljs still works #

One array method finds NaN, another swears it is not there.

In 2026: Still works. indexOf uses strict equality, and since NaN === NaN is false, it can never find NaN and returns -1. includes was added later using SameValueZero, which treats NaN as equal to NaN, so it returns true.

Where it came from: indexOf predates includes and uses strict equality, which NaN fails; includes uses the newer SameValueZero, which matches NaN. MDN's includes page notes the difference.
MDN

<script>
document.write("<p><code>[NaN].indexOf(NaN)</code> is <b>" + [NaN].indexOf(NaN) + "</b></p>");
document.write("<p><code>[NaN].includes(NaN)</code> is <b>" + [NaN].includes(NaN) + "</b></p>");
document.write("<p>indexOf uses ===, includes uses SameValueZero.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

typeof Returns A String

luljs still works #

Ask for the type of a type and you always get "string".

In 2026: Still works. typeof always returns a string naming the type. So typeof 1 is "number", and typeof "number" is "string". That means typeof typeof anything is always "string", no matter what you start with.

Where it came from: The typeof operator always yields a string, so applying it twice can only ever return "string", as MDN's typeof page shows.
MDN

<script>
document.write("<p><code>typeof 1</code> is <b>" + (typeof 1) + "</b></p>");
document.write("<p><code>typeof typeof 1</code> is <b>" + (typeof typeof 1) + "</b></p>");
document.write("<p><code>typeof typeof anything</code> is always <b>string</b>.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

{} + [] vs [] + {}

luljs still works #

Swap the order of an empty object and an empty array and the answer flips from a string to zero.

In 2026: Still works, and it depends on context. As an expression, [] + {} is "[object Object]": both coerce to strings and join. But {} + [] typed as a statement is 0, because the parser reads the leading {} as an empty code block, not an object, leaving +[] which is 0. This asymmetry is the heart of Gary Bernhardt's Wat talk.

Where it came from: The star of Gary Bernhardt's four-minute Wat talk from CodeMash 2012. The flip is a parsing quirk: a leading {} is a block statement, not an object.
Gary Bernhardt: Wat Wayback: 2ality

<script>
// [] + {} as an expression: both coerce to strings and join.
document.write("<p><code>[] + {}</code> is <b>" + ([] + {}) + "</b></p>");
// {} + [] as a STATEMENT parses the leading {} as an empty code block,
// then runs +[] on its own, which is 0. Spelled out exactly as the parser sees it:
{}
var r = +[];
document.write("<p><code>{} + []</code> as a statement is <b>" + r + "</b></p>");
document.write("<p>The leading <code>{}</code> is an empty block, so only <code>+[]</code> runs, which is 0.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

map(parseInt)

luljs still works #

Map parseInt over three identical strings and get three different answers.

In 2026: Still works. Array.map calls its callback with (value, index, array), and parseInt reads (string, radix). So mapping over three "10" strings runs parseInt("10", 0), parseInt("10", 1), parseInt("10", 2): radix 0 defaults to base 10 giving 10, radix 1 is invalid giving NaN, and "10" in base 2 is 2. Wrap it: x => parseInt(x, 10).

Where it came from: The most-cited map gotcha, warned about directly on MDN's map page: map hands the callback an index, and parseInt reads it as a base.
MDN wtfjs

<script>
var raw = ["10", "10", "10"];
document.write('<p><code>["10", "10", "10"].map(parseInt)</code> is <b>[' + raw.map(parseInt).join(', ') + ']</b></p>');
document.write('<p>Same string three times. map hands parseInt the index as a radix.</p>');
document.write('<p>Fixed: <code>.map(function (x) { return parseInt(x, 10); })</code> is <b>[' + raw.map(function(x){return parseInt(x,10);}).join(', ') + ']</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

parseInt(null, 24)

luljs still works #

parseInt of null in base 24 is 23.

In 2026: Still works. parseInt coerces its first argument to a string, so null becomes the text "null". In base 24 the digits run 0 to 9 then a to n, so "n" is worth 23. parseInt reads "n", stops at the first invalid character "u", and returns 23.

Where it came from: A wtfjs classic: parseInt stringifies null to "null", and base 24 makes "n" a valid digit worth 23.
wtfjs

<script>
document.write('<p><code>parseInt(null, 24)</code> is <b>' + parseInt(null, 24) + '</b></p>');
document.write('<p>null becomes the string "null". In base 24, "n" is a valid digit worth 23, and parseInt stops at "u".</p>');
document.write('<p><code>parseInt(null, 10)</code> is <b>' + parseInt(null, 10) + '</b> (base 10 has no "n")</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

parseInt On A Tiny Number

luljs still works #

parseInt of a very small decimal returns 5.

In 2026: Still works. parseInt expects a string, so it converts the number first. A tiny value like 0.0000005 stringifies to "5e-7" in exponential form. parseInt reads the leading "5", stops at "e", and returns 5. parseInt was never meant for floats.

Where it came from: parseInt coerces its argument to a string first, and small numbers stringify to exponential form. MDN's parseInt page warns against using it on numbers.
MDN

<script>
document.write('<p><code>String(0.0000005)</code> is <b>"' + String(0.0000005) + '"</b></p>');
document.write('<p><code>parseInt(0.0000005)</code> is <b>' + parseInt(0.0000005) + '</b></p>');
document.write('<p>The number stringifies to "5e-7", and parseInt reads just the "5".</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Math.round Rounds Up

luljs still works #

Round a score of 2.5 and get 3, but round -2.5 and get -2.

In 2026: Still works. Math.round always rounds a half toward positive infinity, not away from zero. So 2.5 goes up to 3 and 0.5 goes up to 1, but -2.5 goes up to -2 and -0.5 goes up to -0. Rounding a column of negative halves quietly biases them one way.

Where it came from: Math.round rounds halves toward positive infinity, documented on MDN, which is why negative halves round the opposite way to positive ones.
MDN

<script>
document.write('<p><code>Math.round(2.5)</code> is <b>' + Math.round(2.5) + '</b>, <code>Math.round(-2.5)</code> is <b>' + Math.round(-2.5) + '</b></p>');
document.write('<p><code>Math.round(0.5)</code> is <b>' + Math.round(0.5) + '</b>, <code>Math.round(-0.5)</code> is <b>' + Math.round(-0.5) + '</b></p>');
document.write('<p>Halves always round toward +Infinity, not away from zero.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

A $1.005 Price Rounds Down

luljs still works #

Round a price of 1.005 to two decimals and it becomes 1.00, not 1.01.

In 2026: Still works, and it is the float problem in disguise. 1.005 cannot be stored exactly; the nearest double is a hair below it, so toFixed rounds down to "1.00". Any money code built on toFixed inherits this, which is why currency is better handled in whole cents.

Where it came from: 1.005 has no exact binary representation, so the stored value is just under it and toFixed rounds down. A corollary of IEEE 754 doubles.
MDN

<script>
document.write('<p><code>(1.005).toFixed(2)</code> is <b>"' + (1.005).toFixed(2) + '"</b></p>');
document.write('<p>The value really stored: <code>(1.005).toFixed(20)</code> is <b>' + (1.005).toFixed(20) + '</b></p>');
document.write('<p>It sits just under 1.005, so it rounds down.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

MIN_VALUE Is Positive

luljs still works #

Number.MIN_VALUE is greater than zero.

In 2026: Still works, and the name is the trap. Number.MIN_VALUE is the smallest positive number the language can represent, around 5e-324, not the most negative number. The most negative is -Number.MAX_VALUE. So Number.MIN_VALUE > 0 is true.

Where it came from: Number.MIN_VALUE is the smallest representable positive value, not the most negative, as MDN spells out.
MDN

<script>
document.write('<p><code>Number.MIN_VALUE</code> is <b>' + Number.MIN_VALUE + '</b></p>');
document.write('<p><code>Number.MIN_VALUE > 0</code> is <b>' + (Number.MIN_VALUE > 0) + '</b></p>');
document.write('<p>The most negative number is <code>-Number.MAX_VALUE</code>, not MIN_VALUE.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Dividing By Zero

luljs still works #

Divide by zero and nothing throws.

In 2026: Still works. Floating point has signed infinities and a NaN, so 1/0 is Infinity, -1/0 is -Infinity, and 0/0 is NaN. None of them throws. Code that assumes division is safe can carry an Infinity or a NaN a long way before anything looks wrong.

Where it came from: IEEE 754 defines signed infinities and NaN, so division by zero yields a value instead of an error, per MDN's division page.
MDN

<script>
document.write('<p><code>1 / 0</code> is <b>' + (1 / 0) + '</b></p>');
document.write('<p><code>-1 / 0</code> is <b>' + (-1 / 0) + '</b></p>');
document.write('<p><code>0 / 0</code> is <b>' + (0 / 0) + '</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Negative Zero

luljs still works #

Zero and minus zero are equal, until they are not.

In 2026: Still works. Floating point has a separate -0. It equals 0 under both == and ===, so it usually hides. But Object.is tells them apart, and dividing by it reveals the sign: 1 / -0 is -Infinity. Math.round(-0.5) returns -0 for exactly this reason.

Where it came from: IEEE 754 gives zero a sign. Kyle Simpson's You Don't Know JS covers where -0 hides and how Object.is exposes it.
You Don't Know JS

<script>
document.write('<p><code>0 === -0</code> is <b>' + (0 === -0) + '</b></p>');
document.write('<p><code>Object.is(0, -0)</code> is <b>' + Object.is(0, -0) + '</b></p>');
document.write('<p><code>1 / -0</code> is <b>' + (1 / -0) + '</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Unary Plus On Arrays

luljs still works #

A leading plus turns some arrays into numbers and others into NaN.

In 2026: Still works. Unary + coerces through ToNumber, which for an array means toString first. An empty array becomes "" then 0. A one-element array becomes that element's string then a number. A two-element array becomes "1,2", which is not a number, so NaN. A plain object becomes "[object Object]", also NaN.

Where it came from: Unary plus coerces through toString, so arrays flatten to a comma string before the number read. JavaScript Garden's types section walks through it.
JavaScript Garden

<script>
document.write('<p><code>+[]</code> is <b>' + (+[]) + '</b>, <code>+[5]</code> is <b>' + (+[5]) + '</b></p>');
document.write('<p><code>+[1, 2]</code> is <b>' + (+[1,2]) + '</b>, <code>+{}</code> is <b>' + (+{}) + '</b></p>');
document.write('<p>Unary + runs the value through toString, then tries to read a number.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Number() Versus parseInt

luljs still works #

Number() reads whitespace, hex and empties in ways parseInt never would.

In 2026: Still works. Number("") and Number(" ") are 0, Number("0x1F") is 31, Number(null) is 0, and Number(undefined) is NaN. parseInt disagrees on almost every one. The two were built for different jobs and never reconciled.

Where it came from: Number() and parseInt use different conversion rules; MDN's Number() page lists what the constructor accepts.
MDN

<script>
function show(label, v){ document.write('<p><code>' + label + '</code> is <b>' + v + '</b></p>'); }
show('Number("")', Number(""));
show('Number("0x1F")', Number("0x1F"));
show('Number(null)', Number(null));
show('Number(undefined)', Number(undefined));
</script>
sandboxed demo · breaks nothing but itselfrestart

Numbers That Spell Words

luljs still works #

Print certain numbers in base 16 and real words fall out.

In 2026: Still works. Number.prototype.toString takes a radix from 2 to 36. Because the hex digits run 0 to 9 then a to f, some numbers spell words: 3735928559 is deadbeef, 12648430 is c0ffee, and 11259375 is abcdef. The same trick converts any number to binary or any base in between.

Where it came from: Number.prototype.toString accepts a radix from 2 to 36, documented on MDN.
MDN

<script>
document.write('<p><code>(3735928559).toString(16)</code> is <b>' + (3735928559).toString(16) + '</b></p>');
document.write('<p><code>(12648430).toString(16)</code> is <b>' + (12648430).toString(16) + '</b></p>');
document.write('<p><code>(11259375).toString(16)</code> is <b>' + (11259375).toString(16) + '</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

"0" Is Truthy And False

luljs still works #

The string zero is truthy, and also loosely equal to false.

In 2026: Still works. A non-empty string is always truthy, so !!"0" is true and if ("0") runs its block. But "0" == false coerces both sides to the number 0, so they compare equal. The same value is true in a condition and equal to false in a comparison.

Where it came from: Truthiness tests string emptiness while == coerces to numbers, so the two disagree on "0". JavaScript Garden's types section covers both rules.
JavaScript Garden

<script>
document.write('<p><code>!!"0"</code> is <b>' + (!!"0") + '</b> (a non-empty string is truthy)</p>');
document.write('<p><code>"0" == false</code> is <b>' + ("0" == false) + '</b> (both coerce to the number 0)</p>');
document.write('<p>Truthiness and == use different rules, so both are true.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

null and undefined

luljs still works #

The two empty values are loosely equal but strictly different.

In 2026: Still works. null == undefined is true by a special rule in the == algorithm; the two are the language's two ways of saying nothing. But they are different types, so null === undefined is false, and typeof null is "object" while typeof undefined is "undefined".

Where it came from: The == algorithm has a clause making null and undefined equal to each other and nothing else, as MDN's null page notes.
MDN

<script>
document.write('<p><code>null == undefined</code> is <b>' + (null == undefined) + '</b></p>');
document.write('<p><code>null === undefined</code> is <b>' + (null === undefined) + '</b></p>');
document.write('<p><code>typeof null</code> is <b>"' + (typeof null) + '"</b>, <code>typeof undefined</code> is <b>"' + (typeof undefined) + '"</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

A List Equals A String

luljs still works #

A shopping list can equal a string it never looked like.

In 2026: Still works. Comparing an array with == against a string coerces the array to its join, so ["milk", "eggs"] becomes "milk,eggs" and matches. An array holding one null or undefined joins to "", so [null] == "" is true.

Where it came from: Under ==, an array is converted to its comma-joined string before comparison; a wtfjs staple.
wtfjs

<script>
document.write('<p><code>["milk", "eggs"] == "milk,eggs"</code> is <b>' + (["milk","eggs"] == "milk,eggs") + '</b></p>');
document.write('<p><code>[null] == ""</code> is <b>' + ([null] == "") + '</b></p>');
document.write('<p>Under ==, the array becomes its comma-joined string first.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Two Empties Are Not Equal

luljs still works #

Two identical-looking objects are never equal.

In 2026: Still works. Objects and arrays compare by reference, not contents. [] === [] is false and ({}) === ({}) is false, because each literal makes a new object. Only the very same object equals itself, which is why deep comparison needs a helper.

Where it came from: Objects compare by identity, not structure, so two separate literals are never equal. JavaScript Garden's equality section explains reference comparison.
JavaScript Garden

<script>
document.write('<p><code>[] === []</code> is <b>' + ([] === []) + '</b></p>');
document.write('<p><code>({}) === ({})</code> is <b>' + (({}) === ({})) + '</b></p>');
var a = []; document.write('<p><code>a === a</code> is <b>' + (a === a) + '</b> (the very same array)</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Integer Keys Cut The Line

luljs still works #

Number-like object keys always come first, in ascending order.

In 2026: Still works, and it surprises people who expect insertion order. Since ES2015 the language sorts integer-like keys ascending and puts them before the string keys, which keep insertion order. Insert gold, 3, silver, 1 and the keys come back as 1, 3, gold, silver. You cannot pin a numeric key in a chosen spot.

Where it came from: Since ES2015 own keys enumerate as integer indices ascending, then string keys in insertion order. Stefan Judis wrote a clear walkthrough.
Stefan Judis

<script>
var medals = {}; medals.gold = 1; medals[3] = 1; medals.silver = 1; medals[1] = 1;
document.write('<p>Inserted in order: <code>gold, 3, silver, 1</code>.</p>');
document.write('<p><code>Object.keys(medals)</code> is <b>' + JSON.stringify(Object.keys(medals)) + '</b></p>');
document.write('<p>Integer-like keys sort to the front, ascending; string keys keep insertion order.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

JSON Drops Things Silently

luljs 2009 still works #

Serialize a user profile and some fields vanish.

In 2026: Still works. JSON.stringify omits any property whose value is undefined or a function, and it turns NaN and Infinity into null. So a profile with an undefined avatar, a greet method, and a NaN score serializes to the name, a null score and the wins count, with the avatar and method gone. Round-tripping data through JSON quietly loses these.

Where it came from: JSON.stringify omits undefined and function values and maps NaN and Infinity to null, as MDN documents.
MDN

<script>
var profile = { name: "Ada", avatar: undefined, greet: function () {}, score: NaN, wins: 3 };
document.write('<p>Profile: <code>{ name, avatar: undefined, greet: function, score: NaN, wins }</code></p>');
document.write('<p><code>JSON.stringify(profile)</code> is <b>' + JSON.stringify(profile) + '</b></p>');
document.write('<p>undefined and functions are dropped; NaN becomes null.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Holes, Not Undefined

luljs still works #

An array can have gaps that its own methods refuse to visit.

In 2026: Still works. A literal like [1, , 3] has a hole, which is not the same as undefined. Its length is 3, but forEach and map skip the hole entirely, and new Array(3) makes three holes, so its map callback never runs. That is why Array(3).fill() exists.

Where it came from: Sparse arrays hold holes that iteration methods skip, distinct from undefined values. MDN's Array page describes sparse arrays.
MDN

<script>
var arr = [1, , 3];
document.write('<p><code>[1, , 3].length</code> is <b>' + arr.length + '</b></p>');
var seen = 0; arr.forEach(function () { seen++; });
document.write('<p><code>forEach</code> visited <b>' + seen + '</b> of 3 slots (the hole is skipped)</p>');
var ran = 0; new Array(3).map(function () { ran++; });
document.write('<p><code>new Array(3).map(fn)</code> ran fn <b>' + ran + '</b> times</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Off By One Join

luljs still works #

Joining an array of four gives three separators.

In 2026: Still works, and it trips people building repeated strings. join places the separator between elements, so an array of length 4 produces 3 of them. Array(4).join("pizza") gives three, and Array(n) is a common way people accidentally get n minus 1 copies.

Where it came from: join inserts the separator between elements, so N elements yield N-1 separators, per MDN's join page.
MDN

<script>
document.write('<p><code>Array(4).join("\ud83c\udf55")</code> is <b>' + Array(4).join("\ud83c\udf55") + '</b> (three, not four)</p>');
document.write('<p><code>["a", "b", "c"].join(" - ")</code> is <b>' + ["a","b","c"].join(" - ") + '</b></p>');
document.write('<p>join goes between elements, so N elements give N minus 1 separators.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Every Key Is A String

luljs still works #

Use objects as keys and they all collide into one.

In 2026: Still works. A plain object turns every key into a string. So obj[true] and obj["true"] are the same slot, obj[1] and obj["1"] collide, and any object used as a key becomes "[object Object]", so two different objects overwrite each other. Use a Map when you need real keys.

Where it came from: Plain object keys are coerced to strings, so non-string keys collide; MDN's property accessors page covers the conversion.
MDN

<script>
var cache = {};
cache[{ id: 1 }] = "first";
cache[{ id: 2 }] = "second";
document.write('<p>Stored under two different objects. <code>cache[{ id: 1 }]</code> is <b>' + cache[{id:1}] + '</b></p>');
document.write('<p>Both became the key <b>"[object Object]"</b>, so the second overwrote the first.</p>');
document.write('<p><code>Object.keys(cache)</code> is <b>' + JSON.stringify(Object.keys(cache)) + '</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Emoji Break .length

luljs still works #

A single emoji reports a length of two.

In 2026: Still works. A string's length counts UTF-16 code units, and characters outside the basic range, like most emoji, take two units each. So the length of one emoji is 2. Spreading the string with [...str] iterates by code point and gives 1. String indexing splits the emoji into unusable halves.

Where it came from: JavaScript strings are UTF-16, so astral characters span two code units. Mathias Bynens's "JavaScript has a Unicode problem" is the definitive treatment.
Mathias Bynens

<script>
document.write('<p><code>"\u{1F600}".length</code> is <b>' + "\u{1F600}".length + '</b></p>');
document.write('<p><code>[..."\u{1F600}"].length</code> is <b>' + [..."\u{1F600}"].length + '</b></p>');
document.write('<p>length counts UTF-16 code units; the emoji takes two of them.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Why 100 Sorts Before 99

luljs still works #

As text, "100" is less than "99", but as numbers it is not.

In 2026: Still works. When both operands are strings, < compares them character by character by code point, like dictionary order. "1" comes before "9", so "100" < "99" is true. As numbers the comparison flips. This is why a plain string sort puts file10 before file9.

Where it came from: With two string operands, < compares code point by code point, MDN's less-than page notes, so "100" orders before "99".
MDN

<script>
document.write('<p><code>"100" < "99"</code> is <b>' + ("100" < "99") + '</b> (compared as text)</p>');
document.write('<p><code>100 < 99</code> is <b>' + (100 < 99) + '</b> (compared as numbers)</p>');
document.write('<p>Strings compare character by character, so "1" sorts before "9".</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Exponent Goes Right To Left

luljs still works #

2 ** 3 ** 2 is 512, not 64.

In 2026: Still works. The ** operator is right-associative, the only arithmetic operator that is. So 2 ** 3 ** 2 means 2 ** (3 ** 2), which is 2 ** 9, which is 512. Reading it left to right as (2 ** 3) ** 2 would give 64.

Where it came from: Exponentiation is right-associative, unlike the other arithmetic operators, as MDN's exponentiation page states.
MDN

<script>
document.write('<p><code>2 ** 3 ** 2</code> is <b>' + (2 ** 3 ** 2) + '</b></p>');
document.write('<p><code>2 ** (3 ** 2)</code> is <b>' + (2 ** (3 ** 2)) + '</b> (what it means)</p>');
document.write('<p><code>(2 ** 3) ** 2</code> is <b>' + ((2 ** 3) ** 2) + '</b> (what it looks like)</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Using A Variable Before It Exists

luljs still works #

Read a var above its own line and you get undefined, not an error.

In 2026: Still works with var. A var declaration is hoisted to the top of its function, but its assignment stays put. So reading the variable above its line finds it declared but not yet set, giving undefined instead of a ReferenceError. let and const hoist too but forbid this with a temporal dead zone.

Where it came from: var declarations hoist to the top of their scope while assignments stay in place, so a read above the line sees undefined. MDN's hoisting glossary entry explains it.
MDN

<script>
var result = (function () {
  var before = typeof total;   // total is declared just below
  var total = 100;
  return before;
})();
document.write('<p>Reading <code>total</code> before <code>var total = 100</code> gives <b>"' + result + '"</b>, not an error.</p>');
document.write('<p>The declaration hoists to the top of the function; the assignment stays where it is.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

The Object That Lies About Its Type

luljs still works #

typeof document.all is "undefined", yet document.all is right there.

In 2026: Still works, uniquely. document.all is the old Internet Explorer way to reach page elements. To make ancient feature tests like if (document.all) fail on modern browsers, the standard gives it a special mode: typeof reports "undefined" and it loosely equals both undefined and null, even though the object exists and still works. It is the only value in the language that lies about its own type.

Where it came from: The standard marks document.all with a special IsHTMLDDA behaviour so old browser-sniffing fails safely; MDN's document.all page documents the disguise.
MDN HTML standard

<script>
document.write('<p><code>typeof document.all</code> is <b>"' + (typeof document.all) + '"</b></p>');
document.write('<p><code>document.all == undefined</code> is <b>' + (document.all == undefined) + '</b></p>');
document.write('<p>Yet it works: <code>document.all.length</code> is <b>' + document.all.length + '</b> elements on this page.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

How To Make Christmas Into January

luljs still works #

Ask for month 12 to get December and you land in next January.

In 2026: Still works. The Date constructor numbers months from 0, so December is month 11, not 12. Passing 12 rolls over: new Date(2020, 12, 25) is the 25th of January 2021, not Christmas. Days, by contrast, start at 1. This mismatch causes countless off-by-one date bugs.

Where it came from: Date months are zero-based while days are one-based, and out-of-range months roll over, as MDN's Date constructor page shows.
MDN

<script>
document.write('<p><code>new Date(2020, 11, 25)</code> is <b>' + new Date(2020, 11, 25).toDateString() + '</b> (month 11 is December)</p>');
document.write('<p><code>new Date(2020, 12, 25)</code> is <b>' + new Date(2020, 12, 25).toDateString() + '</b> (month 12 rolled into next year)</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Every Symbol Is Unique

luljs still works #

Two symbols with the same description are still different.

In 2026: Still works. Symbol() makes a brand new unique value every time, even with the same description text. So Symbol("id") === Symbol("id") is false. The description is only a label for debugging, which is what makes symbols safe as collision-proof object keys.

Where it came from: Each Symbol() call returns a fresh unique value regardless of description, per MDN's Symbol page.
MDN

<script>
document.write('<p><code>Symbol("id") === Symbol("id")</code> is <b>' + (Symbol("id") === Symbol("id")) + '</b></p>');
var s = Symbol("id");
document.write('<p><code>s === s</code> is <b>' + (s === s) + '</b> (the same symbol)</p>');
document.write('<p>The description "id" is just a label; each Symbol() call is unique.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

arguments Is Not An Array

luljs still works #

The arguments object looks like an array but has none of its methods.

In 2026: Still works in non-arrow functions. arguments holds the call's arguments and has a length and numeric indices, but its type is "object" and it has no map, filter or forEach. You convert it with Array.from(arguments) or a rest parameter. Arrow functions have no arguments at all.

Where it came from: arguments is an array-like object, not an array; MDN's arguments page lists what it lacks.
MDN

<script>
(function () {
  document.write('<p><code>typeof arguments</code> is <b>"' + (typeof arguments) + '"</b></p>');
  document.write('<p><code>arguments.length</code> is <b>' + arguments.length + '</b></p>');
  document.write('<p><code>Array.isArray(arguments)</code> is <b>' + Array.isArray(arguments) + '</b></p>');
})(1, 2, 3);
</script>
sandboxed demo · breaks nothing but itselfrestart

The Comma Operator

luljs still works #

A comma-separated expression evaluates all of it and returns the last.

In 2026: Still works, and it is rarely seen on purpose. The comma operator evaluates each expression left to right and yields the last one, so (1, 2, 3) is 3. It hides in minified code and in for loops. Do not confuse it with the commas in array literals or argument lists.

Where it came from: The comma operator evaluates each operand and returns the last, as MDN's comma operator page describes.
MDN

<script>
document.write('<p><code>(1, 2, 3)</code> is <b>' + (1, 2, 3) + '</b></p>');
var x = (document.write('<p>The middle expression ran (this line), and...</p>'), 42);
document.write('<p><code>x = (writeThisLine(), 42)</code> gives x = <b>' + x + '</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

The CENTER Tag

dead tags 1994 partly #

A whole element whose only job was to centre everything inside it.

In 2026: It still centres, in every browser, which is why it never quite left. It was the one block-level presentational tag important enough to get its own element, and HTML5 removed it anyway. The modern spelling is text-align for inline things and margin auto for blocks.

Where it came from: Netscape Navigator 1.0, 1994, then HTML 3.2. Deprecated in HTML 4.01, removed from HTML5, still rendered by every browser.
MDN HTML standard

<center>
  <h2>My Home Page</h2>
  <p>Every line in here sits in the middle, text and images alike.</p>
  <img src="https://xbawx.com/inc/img/demo/navbar.gif" width="400" height="40" alt="">
</center>

<!-- The 2026 version -->
<div style="text-align:center">inline things centre with text-align</div>
<div style="width:220px; margin:0 auto; border:1px solid #999">a block uses margin auto</div>
sandboxed demo · breaks nothing but itselfrestart

The return That Returns Nothing

luljs still works #

Put the value on the line after return and the function hands back undefined.

In 2026: Still happens. JavaScript inserts a semicolon at a line break when the code so far is a complete statement, and return on its own is complete. So return followed by a newline returns undefined, and the value on the next line never runs. This is exactly why the opening brace of a returned object must sit on the same line as return.

Where it came from: Automatic semicolon insertion. Douglas Crockford made it a headline rule in JavaScript: The Good Parts, 2008: brace on the same line as return.
MDN ECMA-262

<script>
function total() {
  return
    42;
}
document.write('<p><code>return</code>, newline, then <code>42</code> gives <b>' + total() + '</b></p>');
document.write('<p>A semicolon is inserted after <code>return</code>, so the <code>42</code> is dead code.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

1.toString() Is An Error

luljs still works #

Calling a method on a number literal needs two dots, or a space before the one.

In 2026: Still true. The parser reads the dot after a digit as the start of a decimal, so 1.toString() tries to parse 1.toString as a number and throws. The fixes are 1..toString(2), where the first dot is the decimal point and the second is the call, or (1).toString(2), or 1 .toString(2) with a space.

Where it came from: A consequence of the numeric literal grammar, documented on MDN's Number.prototype.toString page.
MDN

<script>
document.write('<p><code>(1).toString(2)</code> is <b>' + (1).toString(2) + '</b></p>');
document.write('<p><code>255..toString(16)</code> is <b>' + 255..toString(16) + '</b></p>');
document.write('<p><code>1.toString(2)</code> is a <b>SyntaxError</b>: the parser reads <code>1.</code> as a decimal point.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

When undefined Could Change

luljs still works #

There was a time you could set undefined to 42, which is why minifiers still write void 0.

In 2026: The relic half is dead, the habit is not. In old JavaScript undefined was an ordinary writable global, so one stray undefined = 42 anywhere poisoned every check on the page. The void operator takes any expression and returns the real undefined, so void 0 was the safe way to get it. ES5 made the global read-only in 2009, and minifiers still emit void 0 because it is shorter than the word.

Where it came from: The void operator and the history of a reassignable undefined, documented on MDN's void page.
MDN

<script>
document.write('<p><code>void 0 === undefined</code> is <b>' + (void 0 === undefined) + '</b></p>');
document.write('<p><code>void "anything at all"</code> is <b>' + (void "anything at all") + '</b></p>');
document.write('<p>Old engines let you reassign <code>undefined</code>. <code>void 0</code> always returned the real one.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

How Anonymous Functions Recursed

luljs still works #

Before a function needed a name to call itself, it reached itself through arguments.callee.

In 2026: Works in a plain script, banned in strict mode and modules. arguments.callee points at the running function, so an unnamed function could call itself with it. Strict mode threw it out because it blocked optimisation and leaked the function, so the modern answer is to give the function a name and call the name. The demo runs sloppy, so it still works.

Where it came from: arguments.callee and its strict-mode ban, documented on MDN.
MDN

<script>
var factorial = function (n) {
  return n <= 1 ? 1 : n * arguments.callee(n - 1);   // the function calls itself with no name
};
document.write('<p><code>factorial(5)</code>, recursing through <code>arguments.callee</code>, is <b>' + factorial(5) + '</b></p>');
document.write('<p>In strict mode that line throws. The fix is a named function expression.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

The Konami Code

browser tricks 2000 still works #

Up up down down left right left right B A, and the page does something silly.

In 2026: Works unchanged, and it is still a rite of passage. A keydown listener matches the key sequence from Konami's 1986 cheat and fires a hidden surprise. A large share of 2000s sites wired one in. Click the demo to give it focus, then type the sequence: the four arrow keys in that order, then B, then A.

Where it came from: The Konami Code, a cheat from the 1986 Konami game Gradius, documented on Wikipedia. It spread onto web pages as a hidden trick through the 2000s.
Wikipedia

<p id="k" style="font:14px Verdana">Click here first, then type:
<b>up up down down left right left right B A</b></p>

<script>
var seq = [38,38,40,40,37,39,37,39,66,65], pos = 0;
document.addEventListener("keydown", function (e) {
  pos = (e.keyCode === seq[pos]) ? pos + 1 : (e.keyCode === seq[0] ? 1 : 0);
  if (pos === seq.length) {
    pos = 0;
    document.getElementById("k").innerHTML =
      "<b style='color:#ff3d8b;font:bold 22px Verdana'>⭐ CHEAT ACTIVATED ⭐</b>";
    document.body.style.transition = "transform 1s";
    document.body.style.transform = "rotate(360deg)";
  }
});
</script>
sandboxed demo · breaks nothing but itselfrestart

NaN Batman

luljs 2012 still works #

Subtract a number from a word, repeat it across an array, and out comes the Batman theme.

In 2026: Still evaluates the same everywhere. 'wat' - 1 forces a number and fails, giving NaN, which join turns into the text "NaN". Array(16) has fifteen gaps between elements, so join lays down fifteen copies of NaN before the tail, producing the word NaN fifteen times over followed by " Batman!".

Where it came from: Gary Bernhardt's 'Wat' lightning talk, CodeMash 2012, on destroyallsoftware.com. Its closing line.
MDN Wat talk

<script>
var s = Array(16).join('wat' - 1) + ' Batman!';
document.write("<h2 style='font-family:sans-serif'>" + s + "</h2>");
document.write("<p><code>Array(16).join('wat' - 1) + ' Batman!'</code></p>");
document.write("<p><code>'wat' - 1</code> is <b>NaN</b>, joined across fifteen array gaps.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

JSFuck

luljs 2012 still works #

Any JavaScript program can be written using only the six characters square bracket, round bracket, bang and plus.

In 2026: The building blocks still evaluate the same in every engine. +[] is 0, ![] is false, !![] is true, and !+[]+!+[] is 2, so any number is reachable by counting. Indexing the strings "false", "true" and "undefined" yields letters, as in (![]+[])[+[]] giving "f", and the Function constructor turns letters back into runnable code. Those steps are enough to write any program with six symbols.

Where it came from: JSFuck by Martin Kleppe, jsfuck.com, 2012.
jsfuck.com github: aemkei/jsfuck

<script>
function row(expr, val){ document.write('<p><code>' + expr + '</code> is <b>' + val + '</b></p>'); }
row('+[]', JSON.stringify(+[]));
row('![]', JSON.stringify(![]));
row('!![]', JSON.stringify(!![]));
row('!+[]+!+[]', JSON.stringify(!+[]+!+[]));
row('[][[]]', String([][[]]));
row('(![]+[])[+[]]', JSON.stringify((![]+[])[+[]]));
document.write('<p>Six characters, <code>[ ] ( ) ! +</code>, reach every number, letter and function.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Adding Two Arrays

luljs 2011 still works #

[1, 2, 3] + [4, 5, 6] returns the string "1,2,34,5,6" instead of adding anything.

In 2026: Every engine still returns "1,2,34,5,6". The plus operator has no array mode, so each operand runs toString first and each array becomes a comma-joined string. Joining "1,2,3" to "4,5,6" sets the 3 that ends the first list against the 4 that starts the second, so the two lists read as 34 with no comma between them.

Where it came from: JavaScript Garden by Ivo Wetzel and Zhang Yi Jiang, the types section, around 2011.
MDN JavaScript Garden

<script>
document.write("<p><code>[1, 2, 3] + [4, 5, 6]</code> is <b>" + ([1,2,3] + [4,5,6]) + "</b></p>");
document.write("<p>Plus has no array mode, so each side runs toString and the two comma strings join.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

One Object Equals One, Two and Three

luljs 2018 still works #

One object can satisfy a == 1 && a == 2 && a == 3 at the same time.

In 2026: It still returns true. Loose == coerces the object by calling valueOf, and this valueOf returns 1, then 2, then 3 as it increments a counter. Each comparison reads the next value, so all three hold in a single expression.

Where it came from: A 2018 Stack Overflow question, among the highest-voted JavaScript questions on the site.
MDN Wayback: Stack Overflow

<script>
var a = { i: 1, valueOf: function(){ return this.i++; } };
document.write("<p><code>a == 1 && a == 2 && a == 3</code> is <b>" + (a == 1 && a == 2 && a == 3) + "</b></p>");
document.write("<p>Each <code>==</code> calls valueOf, and valueOf hands back 1, then 2, then 3.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart

parseInt Reads 0x As Hex

luljs 1999 still works #

parseInt reads a leading 0x as hexadecimal on its own, but only when you leave the radix off.

In 2026: Still works. With no radix argument, parseInt inspects the string and treats a leading 0x or 0X as base 16, so parseInt('0x10') is 16. Pass an explicit radix of 10 and the same string returns 0, because base 10 has no x and parseInt stops at the first character it cannot use. Always pass the radix you mean.

Where it came from: David Flanagan, JavaScript: The Definitive Guide, the parseInt entry documenting the 0x auto-detection (6th edition, around 2011).
MDN

<script>
document.write('<p><code>parseInt("0x10")</code> is <b>' + parseInt("0x10") + '</b> (the 0x makes it hex)</p>');
document.write('<p><code>parseInt("0x10", 10)</code> is <b>' + parseInt("0x10", 10) + '</b> (base 10 stops at the x)</p>');
document.write('<p><code>parseInt("0xFF")</code> is <b>' + parseInt("0xFF") + '</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

The Two Kinds Of isNaN

luljs 1997 still works #

The global isNaN coerces its argument first, so it calls an empty string a number and calls letters not-a-number.

In 2026: Still works. The global isNaN runs its argument through Number() before testing, so isNaN('') is false because Number('') is 0, while isNaN('abc') is true because Number('abc') is NaN. Number.isNaN skips the coercion and reports true only for the actual NaN value. Reach for Number.isNaN, or test x !== x.

Where it came from: Nicholas Zakas, Professional JavaScript for Web Developers, the section on isNaN and numeric coercion (around 2012).
MDN MDN Number.isNaN

<script>
function show(l, v){ document.write('<p><code>' + l + '</code> is <b>' + v + '</b></p>'); }
show('isNaN("")', isNaN(""));
show('isNaN("abc")', isNaN("abc"));
show('isNaN([])', isNaN([]));
show('Number.isNaN("abc")', Number.isNaN("abc"));
</script>
sandboxed demo · breaks nothing but itselfrestart

The Leading Zero That Meant Octal

luljs 1999 still works #

A number written with a leading zero was read in octal, so 010 is 8 and 0755 is 493.

In 2026: Still works in sloppy mode. A numeric literal with a leading zero was read as octal, so 010 is 8 and 0755 is 493, the old Unix file-permission value. ECMAScript 5 strict mode made these a SyntaxError, and the modern replacement is the explicit 0o prefix. A digit of 8 or 9 voids the octal reading and falls back to decimal, so 09 is just 9.

Where it came from: Nicholas Zakas, Professional JavaScript for Web Developers, on octal and hexadecimal literals and the strict-mode ban (around 2012).
MDN

<script>
document.write('<p><code>010</code> is <b>' + 010 + '</b> (a leading zero once meant octal)</p>');
document.write('<p><code>0755</code> is <b>' + 0755 + '</b> (the old file-permission octal)</p>');
document.write('<p>In strict mode <code>010</code> is a <b>SyntaxError</b>: legacy octal literals were banned.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Minus One Is Four Billion

luljs 1997 still works #

Shift minus one right by zero unsigned bits and it becomes four billion.

In 2026: Still works. The >>> operator converts its left side to a 32-bit unsigned integer, so the all-ones bit pattern of -1 reads as 4294967295 instead of a negative number. The signed >> keeps the sign, so -1 >> 0 stays -1, and reading 4294967295 back through | 0 returns -1 from the identical bits. It is the standard way to force an unsigned 32-bit value in JavaScript.

Where it came from: David Flanagan, JavaScript: The Definitive Guide, on the bitwise shift operators and 32-bit integer conversion (around 2011).
MDN

<script>
document.write('<p><code>-1 >>> 0</code> is <b>' + (-1 >>> 0) + '</b> (all 32 bits, read unsigned)</p>');
document.write('<p><code>-1 >> 0</code> is <b>' + (-1 >> 0) + '</b> (signed shift keeps the sign)</p>');
document.write('<p><code>4294967295 | 0</code> is <b>' + (4294967295 | 0) + '</b> (the same bits, read signed)</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

The Safe typeof

luljs 1996 still works #

Ask for the type of a variable that was never declared and you get the string undefined, not an error.

In 2026: Reading an undeclared name anywhere else throws a ReferenceError. typeof is the one operator specified to swallow that error and report the string undefined instead. Old scripts leaned on this: typeof x != 'undefined' was the only safe test for whether a global existed before touching it.

Where it came from: Axel Rauschmayer, Speaking JavaScript (2014), the typeof operator chapter.
MDN Wayback: Speaking JavaScript

<script>
document.write('<p><code>typeof neverDeclared</code> is <b>' + (typeof neverDeclared) + '</b></p>');
try {
  neverDeclared;
  document.write('<p>reading it directly did not throw</p>');
} catch (e) {
  document.write('<p>reading <code>neverDeclared</code> directly throws <b>' + e.name + '</b></p>');
}
document.write('<p>typeof is the one operator that tolerates a name that was never declared.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Truthy Yet Equal To false

luljs 2011 still works #

An empty array passes an if test yet is loosely equal to false at the same time.

In 2026: An if test asks whether the value is truthy, and every object including [] is truthy, so the block runs. Loose == takes a different path: it turns [] into an empty string, then into the number 0, and false is also 0, so [] == false is true. One value, two operators, two opposite answers.

Where it came from: Kyle Simpson, You Don't Know JS Yet: Types & Grammar, the Coercion chapter (falsy objects versus loose equality).
MDN You Don't Know JS

<script>
document.write('<p><code>[] ? "truthy" : "falsy"</code> is <b>' + ([] ? 'truthy' : 'falsy') + '</b></p>');
document.write('<p><code>[] == false</code> is <b>' + ([] == false) + '</b></p>');
document.write('<p>if() asks whether the value is truthy, == turns [] into the number 0. The two rules disagree.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

One Family, Eleven Code Units

luljs 2015 still works #

The family emoji is one glyph but its .length is eleven and it spreads to seven code points.

In 2026: Modern browsers on a supporting OS draw the whole sequence as one family glyph, older systems show four separate people. JavaScript length counts UTF-16 code units, so it reports 11. Spreading iterates by code point and reports 7. Neither equals the single grapheme a reader sees, which is what Intl.Segmenter counts.

Where it came from: Unicode Technical Standard #51, Emoji (2015), which defines ZWJ sequences. The family is U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466.
MDN Unicode UTS 51

<script>
var P = String.fromCodePoint, ZWJ = String.fromCodePoint(0x200D);
var fam = P(0x1F468) + ZWJ + P(0x1F469) + ZWJ + P(0x1F467) + ZWJ + P(0x1F466);
document.write('<p>This renders as one glyph: <b style="font-size:1.7em">' + fam + '</b></p>');
document.write('<p><code>str.length</code> (UTF-16 code units) is <b>' + fam.length + '</b></p>');
document.write('<p><code>[...str].length</code> (code points) is <b>' + [...fam].length + '</b></p>');
document.write('<p>Four person emoji joined by three zero width joiners: eleven code units, one family.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

The Dash Is UTC, The Slash Is Local

luljs 2009 still works #

Swap the date separators from dashes to slashes and midnight jumps by your timezone offset.

In 2026: Every current engine parses a date-only ISO string as UTC, per the ECMAScript Date Time String Format. A slash string is not part of that format, so engines fall back to local time by convention. A browser west of UTC shows the dashed date as the previous evening, the classic 'my date is a day early' bug. The dashed form's toISOString is fixed at midnight Z, only the local rendering shifts.

Where it came from: The ECMAScript Date Time String Format (ECMA-262), where a date-only ISO string is UTC and a slash date is non-standard local time. A staple of timezone write-ups through the 2010s.
MDN ECMA-262

<script>
var dash = new Date("2024-01-01");
var slash = new Date("2024/01/01");
document.write('<p><code>new Date("2024-01-01")</code> local: <b>' + dash.toString() + '</b></p>');
document.write('<p><code>new Date("2024/01/01")</code> local: <b>' + slash.toString() + '</b></p>');
document.write('<p>The dashed ISO date is read as UTC midnight. Its <code>.toISOString()</code> is always <b>' + dash.toISOString() + '</b>. The slashed date is read as LOCAL midnight.</p>');
document.write('<p>Gap in your timezone: <b>' + ((slash - dash) / 3600000) + '</b> hours (0 only on UTC).</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Invalid Date Is Still A Date

luljs 1996 still works #

An unparseable date is not an error, it is a real Date object whose value is NaN and does not equal itself.

In 2026: An unparseable date string does not throw. It produces a real Date object whose internal time value is NaN, so it stringifies to 'Invalid Date' and coerces to NaN. Because NaN never equals NaN, the object's numeric value does not equal itself. Detect it with Number.isNaN(+d), not by comparing dates.

Where it came from: The 'Invalid Date' value in the ECMAScript Date specification (ECMA-262). Discussed in Dr. Axel Rauschmayer's date writing.
MDN

<script>
var d = new Date("spaghetti");
document.write('<p><code>new Date("spaghetti")</code> prints <b>' + String(d) + '</b></p>');
document.write('<p><code>typeof</code> it is <b>' + typeof d + '</b>, <code>instanceof Date</code> is <b>' + (d instanceof Date) + '</b></p>');
document.write('<p>Its timestamp <code>+d</code> is <b>' + (+d) + '</b></p>');
document.write('<p>By value it does not even equal itself: <code>+d === +d</code> is <b>' + (+d === +d) + '</b></p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

The Table Body You Never Typed

parser and markup 1997 still works #

Write a table with only tr rows and the parser slips a tbody between the table and every row.

In 2026: Still happens everywhere. The HTML parsing algorithm opens an implied tbody the moment a tr appears with no section element around it, so table.firstElementChild is a tbody you never wrote. It is why the CSS selector table > tr matches nothing, and why scripts must append new rows to the tbody rather than the table. thead and tfoot are the only sections the parser will not invent.

Where it came from: PPK (Peter-Paul Koch) documented the scripting consequence on quirksmode.org in the early 2000s. The insertion itself lives in the WHATWG parser in table insertion mode.
MDN HTML parser spec

<table id="t" border="1">
  <tr><td>a row with no tbody in the source</td></tr>
</table>
<script>
var t = document.getElementById('t');
document.write('<p>t.tBodies.length is <b>' + t.tBodies.length +
  '</b>, and the table first child is a &lt;' +
  t.firstElementChild.tagName.toLowerCase() + '&gt;.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Entities Without The Semicolon

parser and markup 1998 still works #

&copy renders the copyright sign with no closing semicolon, and &notit; quietly becomes the not sign followed by the text it;.

In 2026: Still works, and still bites. A short legacy list of named references (copy, amp, lt, gt, reg, nbsp and a few dozen more) is matched even without the trailing semicolon, carried from HTML 4 into the modern parser. Matching is greedy, so &notit; finds &not and leaves it; behind. A newer name with no legacy prefix, like &bigstar, renders as the literal text without its trailing semicolon; only &bigstar; gives the star.

Where it came from: The WHATWG HTML standard, named character references table and the ambiguous ampersand parse error. The semicolon-optional set is inherited from HTML 4.0 (W3C, 1998).
WHATWG

<p>Named references still resolve with the semicolon left off:</p>
<p style="font-size:1.6em">&copy &reg &amp &lt &gt</p>
<p>The greedy trap. The source <code>&amp;notit;</code> renders as
<b>&notit;</b>, because the parser matches <code>&amp;not</code>
(the not sign) and leaves <code>it;</code> as plain text.</p>
sandboxed demo · breaks nothing but itselfrestart

The End Tags You Can Skip

parser and markup 1995 still works #

A li with no closing tag ends at the next li, and a p ends at the next p, because their end tags are optional.

In 2026: Still valid HTML. The standard lists p, li, dt, dd, tr, td, option and more as elements whose end tag may be omitted, and the parser closes each one when the next sibling or a block element starts. Three li lines with no closing tags parse as three separate items. A p cannot contain another p, so a second p tag closes the first.

Where it came from: The WHATWG HTML standard, Optional tags section. The habit goes back to hand-written HTML in the mid 1990s, before editors closed tags for you.
WHATWG

<ul>
  <li>first
  <li>second
  <li>third
</ul>
<script>
document.write('<p>List items parsed: <b>' +
  document.querySelectorAll('ul li').length +
  '</b>. Each &lt;li&gt; closed itself at the next one.</p>');
</script>
sandboxed demo · breaks nothing but itselfrestart

Ruby Text Above The Characters

parser and markup 1998 still works #

The ruby element prints a small pronunciation gloss riding directly above each base character.

In 2026: Still works in every current browser. ruby wraps the base text and rt holds the annotation, which the browser sets in small type above the base (furigana over kanji, pinyin over hanzi). Microsoft shipped it in Internet Explorer 5 before it was a standard. It is one of the few 1990s vendor markup features that reached the modern spec almost unchanged.

Where it came from: Microsoft added ruby in Internet Explorer 5 (1999). The W3C published Ruby Annotation as a Recommendation in 2001, and HTML5 adopted the element.
MDN W3C Ruby Annotation

<p style="font-size:2.4em; line-height:2.6">
  <ruby>漢<rt>kan</rt>字<rt>ji</rt></ruby>
  and
  <ruby>東京<rt>tokyo</rt></ruby>
</p>
sandboxed demo · breaks nothing but itselfrestart

The run-in Display Value

layout hacks 1998 dead #

CSS2 had a display value that folded a heading into the paragraph after it, so a run-in title sat inline at the start of the next block.

In 2026: Dead. No current browser supports run-in, so the heading below renders as its own block line instead of joining the paragraph. CSS Display Level 3 dropped the value, and the engines that once shipped it (old Opera Presto, IE8, an early Blink) no longer do.

Where it came from: CSS2 run-in display, W3C 1998. Worked through in Eric Meyer, CSS: The Definitive Guide. Removed by CSS Display Level 3.
MDN caniuse CSS2 spec

<style>
/* CSS2 run-in: a heading meant to fold into the paragraph after it */
h3.runin { display: run-in; font-weight: bold; margin: 0; }
</style>

<h3 class="runin">Ingredients.</h3>
<p>Flour, water, salt. In 1998 this heading was meant to sit
   inline at the very start of this paragraph, as a run-in title.
   Today it renders as its own block line above, because no
   browser supports run-in.</p>
sandboxed demo · breaks nothing but itselfrestart

Flexbox Before It Was Flex

layout hacks 2009 partly #

The 2009 flexbox draft used display: box with box-orient and box-flex, a whole different syntax from the display: flex that replaced it.

In 2026: Partly alive. Blink and WebKit still honour the prefixed display: -webkit-box, which is why -webkit-line-clamp works, so the row below still lays out. The unprefixed display: box and box-flex are dead, and Firefox no longer supports -moz-box for web content. The modern spelling is display: flex.

Where it came from: CSS Flexible Box Layout, 2009 W3C working draft. Chris Coyier covered the old display: box syntax on CSS-Tricks before the 2012 rewrite.
MDN 2009 W3C draft CSS-Tricks

<style>
/* The 2009 flexbox draft: display: box, not flex */
.oldflex {
  display: -webkit-box;   /* still honoured by Blink and WebKit */
  display: box;           /* the unprefixed 2009 word, dead */
  -webkit-box-orient: horizontal;
  box-orient: horizontal;
}
.oldflex > div { -webkit-box-flex: 1; box-flex: 1; padding: 6px; }
</style>

<div class="oldflex">
  <div style="background:#cde">box-flex 1</div>
  <div style="background:#dec">box-flex 1</div>
  <div style="background:#edc">box-flex 1</div>
</div>
sandboxed demo · breaks nothing but itselfrestart

Hidden By clip: rect

layout hacks 1998 partly #

The classic accessible-hide recipe pinned an element to one pixel and clipped it away, keeping it in the page for screen readers but off the screen.

In 2026: Partly. The clip property still works, so the phrase below is present for assistive technology and invisible to the eye. clip is deprecated in favour of clip-path: inset(50%), but browsers keep obeying it. clip only affects absolutely positioned elements, which is why the recipe also sets position: absolute.

Where it came from: The clip visually-hidden pattern, spread through Chris Coyier's CSS-Tricks around 2011. The clip property is CSS2, W3C 1998, now deprecated.
MDN WebAIM

<style>
/* The classic accessible hide: present for readers, off the screen for eyes */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(1px, 1px, 1px, 1px);  /* deprecated, still obeyed */
  white-space: nowrap;
}
</style>

<p>Search <span class="visually-hidden">the entire music archive</span>here.</p>
<p>A screen reader announces "Search the entire music archive here".
   Your eyes see "Search here". clip does the hiding, and
   clip-path: inset(50%) is its replacement.</p>
sandboxed demo · breaks nothing but itselfrestart

Numbers From The Stylesheet

text effects 1998 still works #

counter-reset and counter-increment let CSS number a list with content: counter(x), so the digits live in the stylesheet and not the markup.

In 2026: Alive. This CSS2 feature still works everywhere, and the list below numbers itself with no numerals typed in the HTML. Each item increments a named counter, and the ::before pulls its value in as generated content. The same mechanism drives nested section numbers with counters(x, '.').

Where it came from: CSS2 generated content and automatic counters, W3C 1998. Explained in Eric Meyer, CSS: The Definitive Guide.
MDN CSS2 spec

<style>
/* Numbering with no numbers in the markup, CSS2 */
ol.steps { list-style: none; counter-reset: step; padding-left: 0; }
ol.steps > li { counter-increment: step; margin: 2px 0; }
ol.steps > li::before {
  content: "Step " counter(step) ": ";
  font-weight: bold;
}
</style>

<ol class="steps">
  <li>Insert coin.</li>
  <li>Press start.</li>
  <li>Blow into the cartridge.</li>
</ol>
sandboxed demo · breaks nothing but itselfrestart

When :hover Only Worked On Links

browser wars 2001 partly #

Internet Explorer 6 applied :hover only to anchor elements, so a rule like li:hover did nothing and pure-CSS dropdown menus were impossible there.

In 2026: Partly, by era. Both the list item and the link below highlight on hover in any current browser. In IE6 only the link would have reacted, because that browser limited :hover and :active to <a>. Authors bolted the behaviour onto other elements with Peter Nederlof's csshover.htc, a scripted behavior file.

Where it came from: The IE6 :hover limitation documented on Peter-Paul Koch's quirksmode.org. Worked around by Peter Nederlof's csshover.htc, around 2004.
MDN quirksmode

<style>
/* In IE6 this list-item rule did nothing. :hover worked only on <a>. */
li.menu:hover { background: #fe9; }
a.old:hover  { background: #fe9; }  /* the only :hover IE6 obeyed */
</style>

<ul>
  <li class="menu">Hover this list item. Dead in IE6, works now.</li>
</ul>
<p><a class="old" href="#">Hover this link. Worked even in IE6.</a></p>
sandboxed demo · breaks nothing but itselfrestart

The CSS Triangle

pure css 2006 still works #

A triangle drawn from the borders of a box with no width and no height.

In 2026: A box with zero width and height has borders that meet along diagonals. Make three sides transparent and one side solid, and the solid side is a triangle. Every browser still renders it, and it was the standard way to draw a callout arrow or a dropdown caret before clip-path and inline SVG were common.

Where it came from: Chris Coyier, CSS-Tricks 'CSS Triangle', roughly 2009. The border technique circulated on CSS forums before that.
CSS-Tricks

<style>
.tri-up {
  width: 0; height: 0;
  border-left: 30px solid transparent;
  border-right: 30px solid transparent;
  border-bottom: 52px solid #c0392b;
}
.tri-right {
  width: 0; height: 0;
  border-top: 30px solid transparent;
  border-bottom: 30px solid transparent;
  border-left: 52px solid #2c7d4f;
  margin-top: 18px;
}
</style>

<p>An element with zero width and zero height. Its four borders meet along 45 degree diagonals, so one solid border with its neighbours transparent leaves a triangle.</p>
<div class="tri-up"></div>
<div class="tri-right"></div>
sandboxed demo · breaks nothing but itselfrestart

The Checkbox Hack

pure css 2011 still works #

Clickable open/close state stored in a hidden checkbox and read with :checked ~ sibling.

In 2026: A hidden checkbox holds the state, a label flips it, and the :checked sibling combinator styles a later element. It works in every browser and is the base of CSS-only menus, tabs and accordions. The catch is that the target must be a later sibling of the checkbox, which forces an awkward markup order.

Where it came from: Chris Coyier, 'The Checkbox Hack', CSS-Tricks, 2011.
CSS-Tricks

<style>
.toggle-box { position: absolute; left: -9999px; }
.toggle-label {
  display: inline-block; cursor: pointer;
  background: #234a6b; color: #fff; padding: 8px 16px;
}
.toggle-panel {
  max-height: 0; overflow: hidden;
  background: #eef2f7; color: #12233a;
  transition: max-height .3s ease;
}
.toggle-box:checked ~ .toggle-panel { max-height: 200px; padding: 12px; }
</style>

<input type="checkbox" id="t1" class="toggle-box">
<label for="t1" class="toggle-label">Click to toggle</label>
<div class="toggle-panel">
  Hidden until the checkbox is checked. A label flips a hidden checkbox, and the
  sibling combinator reveals this panel. No JavaScript.
</div>
sandboxed demo · breaks nothing but itselfrestart

The Pure CSS Dropdown Menu

pure css 2003 still works #

A navigation dropdown that opens on hover using only nested lists and :hover.

In 2026: The submenu is a nested list set to display:none and revealed by li:hover > ul. Browsers still do this, but a touch screen has no hover state, so real menus later added a click or focus fallback. Before jQuery this was the whole dropdown.

Where it came from: Patrick Griffiths and Dan Webb, 'Suckerfish Dropdowns', A List Apart, 2003, building on Eric Meyer's Pure CSS Menus.
A List Apart Eric Meyer demo

<style>
.nav, .nav ul { list-style: none; margin: 0; padding: 0; }
.nav > li { position: relative; display: inline-block;
            background: #234a6b; color: #fff; padding: 6px 14px; }
.nav ul { position: absolute; left: 0; top: 100%;
          display: none; min-width: 150px; z-index: 5; }
.nav ul li { background: #336789; padding: 6px 14px;
             border-top: 1px solid #234a6b; }
.nav li:hover > ul { display: block; }
</style>

<ul class="nav">
  <li>Home</li>
  <li>Products
    <ul>
      <li>Widgets</li>
      <li>Gadgets</li>
      <li>Gizmos</li>
    </ul>
  </li>
  <li>About</li>
</ul>
<p>Hover "Products". The submenu drops with no script. This was the navigation dropdown before jQuery.</p>
sandboxed demo · breaks nothing but itselfrestart

A Lightbox With :target

pure css 2008 still works #

A modal overlay shown by the :target pseudo-class when the URL fragment points at it.

In 2026: Clicking a link to #id changes the URL fragment, :target matches the element with that id, and CSS reveals it. Browsers still support it, and because the state lives in the URL it adds a real back-button entry. It powered CSS-only modals and tab sets before anyone reached for script.

Where it came from: The :target pseudo-class, CSS Selectors Level 3, W3C, 2001. Popularised for tabs and lightboxes by Chris Coyier and Roman Komarov around 2012.
MDN

<!DOCTYPE html>
<html>
<head>
<!-- The base keeps the #shot link resolving inside this sandboxed preview.
     You do not need it on your own page, delete this line there. -->
<base href="about:srcdoc">
<style>
.lb { position: fixed; top: 0; left: 0; right: 0; bottom: 0;
      background: rgba(0,0,0,.8);
      display: none; align-items: center; justify-content: center; }
.lb:target { display: flex; }
.lb .card { background: #fff; color: #12233a; padding: 28px; max-width: 300px; }
.lb .close { display: inline-block; margin-top: 12px; color: #c0392b; }
</style>
</head>
<body>
<p><a href="#shot">Open the lightbox</a></p>
<div class="lb" id="shot">
  <div class="card">
    A modal with no JavaScript. The link sets the URL fragment to this box's id,
    :target matches, and the box appears.
    <a class="close" href="#">Close</a>
  </div>
</div>
</body>
</html>
sandboxed demo · breaks nothing but itselfrestart

Engraved And Embossed Text

text effects 2010 still works #

A carved or raised look faked with a single one-pixel text-shadow, no blur.

In 2026: A one-pixel light highlight directly below dark text reads as engraved, and a one-pixel light highlight directly above near-background text reads as raised. There is no blur, so it is a crisp offset, not the blurred bloom of a glow. Browsers render it identically today.

Where it came from: The letterpress text technique, popularised on CSS-Tricks and Web Designer Wall around 2010, once text-shadow was widely supported.
MDN

<style>
.press {
  background: #d3d7de; color: #4a4f57;
  font: bold 2.2rem/1.4 Arial, sans-serif;
  text-align: center; padding: 22px;
  text-shadow: 0 1px 0 #ffffff;
}
.emboss {
  background: #2b2f36; color: #2f333b;
  font: bold 2.2rem/1.4 Arial, sans-serif;
  text-align: center; padding: 22px;
  text-shadow: 0 -1px 0 rgba(255,255,255,.45);
}
</style>

<p class="press">ENGRAVED</p>
<p class="emboss">EMBOSSED</p>
sandboxed demo · breaks nothing but itselfrestart

The Drop Cap

text effects 1996 still works #

An enlarged, floated opening letter styled with ::first-letter and no extra markup.

In 2026: ::first-letter selects the opening character, which is not an element, and floating it while enlarging it makes a drop cap. It has worked since CSS1 in 1996 and every browser honours it. The spec folds any leading punctuation into the same selection, so a quote mark before the letter is styled too.

Where it came from: ::first-letter, CSS Level 1, W3C, 1996. One of the original pseudo-elements in the first CSS spec.
MDN CSS1 spec

<style>
.dropcap::first-letter {
  float: left;
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 3.4em; line-height: .8;
  padding: 4px 8px 0 0; color: #a33;
  font-weight: bold;
}
</style>

<p class="dropcap">Once upon a time the first letter of a chapter was drawn large
and set into the paragraph. The ::first-letter pseudo-element does it with no
extra markup, styling a character that is not an element. It has been in CSS
since the first version.</p>
sandboxed demo · breaks nothing but itselfrestart

Styling The Console With Percent C

browser tricks 2012 still works #

A %c token in a console.log makes the console read the next argument as CSS and style the text.

In 2026: Still works in every DevTools console. The %c format directive consumes the following argument as a CSS declaration string and applies it to the log text that follows. Only typography and box properties take effect (color, background, font, padding); layout and positioning are ignored. It is how tools print oversized coloured banners in the console.

Where it came from: MDN Console API, styling console output. The %c directive came from the Firebug console around 2010 and was standardised into the WHATWG console standard.
MDN WHATWG console

<script>
// Open the browser DevTools console (F12) to see the styled line.
console.log(
  "%cxbawx.com",
  "color:#fff;background:#c0f;font-size:40px;padding:6px 14px;border-radius:6px;font-family:monospace"
);
document.write("<p>Open your DevTools console (F12). This page logged a 40px pink banner:</p>");
document.write("<pre>console.log(\"%cxbawx.com\", \"color:#fff;background:#c0f;font-size:40px\")</pre>");
document.write("<p>The <code>%c</code> token reads the NEXT argument as a CSS string and paints everything after it. Two <code>%c</code> tokens style two runs.</p>");
</script>
sandboxed demo · breaks nothing but itselfrestart