[X]

</> DHTML Snippets

203 pieces of obsolete, non-standard, plainly weird markup from the old web · 99 still work · 45 partly · 59 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, or copy to grab the source. 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 stopped letting scripts write window.status around 2010 because it was used to fake link targets. 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

<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 but it is non-standard and formally obsolete. Use CSS animation if you want it to survive.

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

<marquee behavior="scroll" direction="left" scrollamount="6"
         bgcolor="#000080" width="100%">
  <font face="Comic Sans MS" color="#FFFF00" size="4">
    *** WELCOME TO MY HOMEPAGE *** SIGN MY GUESTBOOK ***
  </font>
</marquee>

<marquee behavior="alternate" scrollamount="4" width="60%">
  bouncing text!
</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

<!-- The original (Netscape only, long dead) -->
<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

<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

<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>
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

<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

<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

<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

<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

<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  \u1F383 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

<canvas id="stars" style="position:fixed;inset:0;z-index:-1;background:#000"></canvas>

<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

<!-- 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 just 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

<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

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

<!-- 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

<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. Set TARGET to any date you like.

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("January 1, 2000 00:00:00");
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

<!-- 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">
if (document.cookie.indexOf("seenwelcome=1") === -1) {
  alert("Welcome to my home page!");
  document.cookie = "seenwelcome=1; path=/; max-age=31536000";
}
</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>
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;
var m = document.cookie.match(/visitorname=([^;]+)/);
if (m) {
  who = decodeURIComponent(m[1]);
} else {
  who = prompt("Hi! What's your name?", "");
  if (who) document.cookie = "visitorname=" + encodeURIComponent(who)
                           + "; path=/; max-age=31536000";
}
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

<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 is now the physical display, not the browser window. 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 -->
<script>
document.write("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. HTTPS sites strip the referrer by default when sending you to another origin.

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

<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 behavior: property that any page could call, and it was abused constantly. No browser has allowed it since IE 8.

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>

<!-- 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 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

<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

<center>
<img src="https://xbawx.com/btn/assets/a0/a07ac27c9b25023589627784791ed6c4bec42515.gif"
     width="120" height="45" 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/a0/a07ac27c9b25023589627784791ed6c4bec42515.gif"
     width="120" height="45" alt="">
</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. Only Netscape 3 and 4 ever supported it, 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

<!-- 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 1993 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

<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, both were deprecated in HTML 3.2. LISTING was defined as 132 columns, XMP as 80. Use PRE.

Where it came from: HTML 2.0 (RFC 1866), 1995. Deprecated in HTML 3.2, 1997. MDN

<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 around 2015. 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

<!-- 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. -->

<!-- And the CFML/SSI style, which the server strips before it ships -->
<!--#comment This one never reaches the browser at all -->
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 is still non-standard and still works in every browser. 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

<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.01 the following year. MDN

<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

<!-- 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

<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 2018. 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

<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

<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

<!-- 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

<body bgcolor="#000000" text="#00FF00" link="#FFFF00" vlink="#FF00FF" alink="#FF0000"
      topmargin="0" leftmargin="0" marginheight="0" marginwidth="0">

<!-- The 2026 version -->
<style>body { margin: 0; background: #000; color: #0f0 }</style>
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, 1998, published on tantek.com and widely reprinted by A List Apart readers around 2002. Wikipedia

<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

<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>

<!-- 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

<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

<!-- 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>
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

<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 2011. 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

<!--[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. Every other browser ran the block immediately as ordinary top level code, which usually threw, so this was a good way to break your page for half the web.

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

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

<!-- IE only. Everyone else executes this as plain script on load. -->
<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 1999 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, 1999. 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 still is not in any standard. 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. The demo below reloads itself every four seconds.

Where it came from: Netscape Navigator 1.1 client pull, 1995. Never made it into an HTML spec. Wikipedia

<!-- Redirect after 5 seconds -->
<meta http-equiv="refresh" content="5;url=http://www.example.com/newpage.html">

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

<!-- No URL: reload this same page. Slideshows and webcams used this. -->
<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

Ten 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

<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

<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

Three tags for switching off three things Internet Explorer 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

<!-- 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. Browsers dropped the plugin API in 2015 and 2016 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

<!-- 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

<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

<!-- 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>
<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 dead

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

In 2026: Broken, and it was broken then too. It needed a configured mail client, it silently did nothing if there was none, the visitor's own address leaked to you, and Netscape and IE encoded the body differently. 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

<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, which some browsers put in the subject line -->
<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, two of which were never standard 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

<!--#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

<!-- 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

<!-- 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

<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

<!-- 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 -->
<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 none of it is in any standard. MARQUEE 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

<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

<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

<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 2.1 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

<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 proudly prints the year 19126. 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

<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 2016, 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 2016. 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

<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. The rival IE model needed no registration at all, the DOM standard followed suit, and captureEvents lingered as a do-nothing stub for years before browsers deleted even the stub. 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

<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 here.</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 2022 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

<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

<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 anything non-ASCII 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

<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

<!-- 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

<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 four declarations 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 half transparent 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

<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

<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. Douglas Bowman's Sliding Doors, A List Apart 2003, was the most-copied version. border-radius ended it around 2010. MDN

<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

<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

<!-- 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 1999 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 Internet Explorer took it with them. The demo reports that createPopup is not a function.

Where it came from: Microsoft window.createPopup, Internet Explorer 4, 1999. 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

<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 in 2021 (Chrome 88, Firefox 79, Safari 12.1), 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, 2020 to 2021. MDN

<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

<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

<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

<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

<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

<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

<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

<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

<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 still works

Serialize a user profile and some fields just 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 just the name and a null score. 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

<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