diff --git a/demo/matchbox.html b/demo/matchbox.html
index 028c2b3..0d61e2c 100644
--- a/demo/matchbox.html
+++ b/demo/matchbox.html
@@ -7,39 +7,10 @@
 
 <h1>Matchbox</h1>
 
-<textarea id="prog1">
-MOV M0, R0
-INC R0
-MOV R0, M0
-</textarea>
-
-<textarea id="prog2">
-MOV M0, R0
-INC R0
-INC R0
-MOV R0, M0
-</textarea>
-
-<button onclick="interleave()">Interleave</button>
-
-<pre id="output">
-</pre>
+<div id="container"></div>
 
 </body>
 <script src="../src/matchbox.js"></script>
 <script>
-"use strict";
-
-var output = document.getElementById('output');
-
-var x = parseCode(document.getElementById('prog1').value);
-var y = parseCode(document.getElementById('prog2').value);
-
-var interleave = function() {
-    output.innerHTML = '';
-    var interleavings = findAllInterleavings(x, y);
-    for (var i = 0; i < interleavings.length; i++) {
-        output.innerHTML += '[' + interleavings[i] + ']\n';
-    }
-};
+launch('../src/yoob/', 'container', {});
 </script>
diff --git a/src/matchbox.js b/src/matchbox.js
index 77cd1e2..1efa709 100644
--- a/src/matchbox.js
+++ b/src/matchbox.js
@@ -1,108 +1,61 @@
 "use strict";
 
-/*
- * A lexical analyzer.
- * Create a new yoob.Scanner object, then call init, passing it an
- * array of two-element arrays; first element of each of these is the
- * type of token, the second element is a regular expression (in a
- * String) which matches that token at the start of the string.  The
- * regular expression should have exactly one capturing group.
- * Then call reset, passing it the string to be scanned.
- * 
- */
-var Scanner = function() {
-  this.text = undefined;
-  this.token = undefined;
-  this.type = undefined;
-  this.error = undefined;
-  this.table = undefined;
-  this.whitespacePattern = "^[ \\t\\n\\r]*";
-
-  this.init = function(table) {
-    this.table = table;
-    return this;
-  };
-
-  this.reset = function(text) {
-    this.text = text;
-    this.token = undefined;
-    this.type = undefined;
-    this.error = undefined;
-    this.scan();
-  };
-  
-  this.scanPattern = function(pattern, type) {
-    var re = new RegExp(pattern);
-    var match = re.exec(this.text);
-    if (match === null) return false;
-    this.type = type;
-    this.token = match[1];
-    this.text = this.text.substr(match[0].length);
-    //console.log(this.type, this.token);
-    return true;
-  };
-
-  this.scan = function() {
-    this.scanPattern(this.whitespacePattern, "whitespace");
-    if (this.text.length === 0) {
-      this.token = null;
-      this.type = "EOF";
-      return;
-    }
-    for (var i = 0; i < this.table.length; i++) {
-      var type = this.table[i][0];
-      var pattern = this.table[i][1];
-      if (this.scanPattern(pattern, type)) return;
-    }
-    if (this.scanPattern("^([\\s\\S])", "unknown character")) return;
-    // should never get here
-  };
-
-  this.expect = function(token) {
-    if (this.token === token) {
-      this.scan();
-    } else {
-      this.error = "expected '" + token + "' but found '" + this.token + "'";
-    }
-  };
-
-  this.on = function(token) {
-    return this.token === token;
-  };
-
-  this.onType = function(type) {
-    return this.type === type;
-  };
-
-  this.checkType = function(type) {
-    if (this.type !== type) {
-      this.error = "expected " + type + " but found " + this.type + " (" + this.token + ")"
-    }
-  };
-
-  this.expectType = function(type) {
-    this.checkType(type);
-    this.scan();
-  };
-
-  this.consume = function(token) {
-    if (this.on(token)) {
-      this.scan();
-      return true;
-    } else {
-      return false;
-    }
-  };
-
-};
-
-var matchboxScanner = (new Scanner()).init([
-    ['immediate', "^(\\d+)"],
-    ['register',  "^([rR]\\d+)"],
-    ['memory',    "^([mM]\\d+)"],
-    ['opcode',    "^([a-zA-Z]+)"],
-    ['comma',     "^(,)"]
-]);
+function launch(prefix, container, config) {
+    if (typeof container === 'string') {
+        container = document.getElementById(container);
+    }
+    config = config || {};
+    var deps = [
+        "scanner.js",
+        "element-factory.js"
+    ];
+    var loaded = 0;
+    for (var i = 0; i < deps.length; i++) {
+        var elem = document.createElement('script');
+        elem.src = prefix + deps[i];
+        elem.onload = function() {
+            if (++loaded < deps.length) return;
+
+            var prog1ta = yoob.makeTextArea(container, 20, 10);
+            var prog2ta = yoob.makeTextArea(container, 20, 10);
+
+            prog1ta.value = "MOV M0, R0\nINC R0\nMOV R0, M0";
+            prog2ta.value = "MOV M0, R0\nINC R0\nMOV R0, M0";
+
+            var interleaveBtn = yoob.makeButton(container, "Interleave");
+
+            var output = yoob.makePre(container);
+
+            initScanner();
+
+            interleaveBtn.onclick = function() {
+                var x = parseCode(prog1ta.value);
+                var y = parseCode(prog2ta.value);
+
+                output.innerHTML = '';
+                var interleavings = findAllInterleavings(x, y);
+                for (var i = 0; i < interleavings.length; i++) {
+                    output.innerHTML += '[' + interleavings[i] + ']\n';
+                }
+            };
+
+        };
+        document.body.appendChild(elem);
+    }
+}
+
+var matchboxScanner;
+
+function initScanner() {
+    matchboxScanner = (new yoob.Scanner());
+    matchboxScanner.init([
+        ['immediate', "^(\\d+)"],
+        ['register',  "^([rR]\\d+)"],
+        ['memory',    "^([mM]\\d+)"],
+        ['opcode',    "^([a-zA-Z]+)"],
+        ['comma',     "^(,)"]
+    ]);
+}
 
 /*
  * Each instruction is an object with some fields:
diff --git a/src/yoob/element-factory.js b/src/yoob/element-factory.js
new file mode 100644
index 0000000..3045bae
--- /dev/null
+++ b/src/yoob/element-factory.js
@@ -0,0 +1,203 @@
+/*
+ * This file is part of yoob.js version 0.8
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * Functions for creating elements.
+ */
+
+yoob.makeCanvas = function(container, width, height) {
+    var canvas = document.createElement('canvas');
+    if (width) {
+        canvas.width = width;
+    }
+    if (height) {
+        canvas.height = height;
+    }
+    container.appendChild(canvas);
+    return canvas;
+};
+
+yoob.makeButton = function(container, labelText, fun) {
+    var button = document.createElement('button');
+    button.innerHTML = labelText;
+    container.appendChild(button);
+    if (fun) {
+        button.onclick = fun;
+    }
+    return button;
+};
+
+yoob.checkBoxNumber = 0;
+yoob.makeCheckbox = function(container, checked, labelText, fun) {
+    var checkbox = document.createElement('input');
+    checkbox.type = "checkbox";
+    checkbox.id = 'cfzzzb_' + yoob.checkBoxNumber;
+    checkbox.checked = checked;
+    var label = document.createElement('label');
+    label.htmlFor = 'cfzzzb_' + yoob.checkBoxNumber;
+    yoob.checkBoxNumber += 1;
+    label.appendChild(document.createTextNode(labelText));
+    
+    container.appendChild(checkbox);
+    container.appendChild(label);
+
+    if (fun) {
+        checkbox.onchange = function(e) {
+            fun(checkbox.checked);
+        };
+    }
+    return checkbox;
+};
+
+yoob.makeTextInput = function(container, size, value) {
+    var input = document.createElement('input');
+    input.size = "" + (size || 12);
+    input.value = value || "";
+    container.appendChild(input);
+    return input;
+};
+
+yoob.makeSlider = function(container, min, max, value, fun) {
+    var slider = document.createElement('input');
+    slider.type = "range";
+    slider.min = min;
+    slider.max = max;
+    slider.value = value || 0;
+    if (fun) {
+        slider.onchange = function(e) {
+            fun(parseInt(slider.value, 10));
+        };
+    }
+    container.appendChild(slider);
+    return slider;
+};
+
+yoob.makeParagraph = function(container, innerHTML) {
+    var p = document.createElement('p');
+    p.innerHTML = innerHTML || '';
+    container.appendChild(p);
+    return p;
+};
+
+yoob.makeSpan = function(container, innerHTML) {
+    var span = document.createElement('span');
+    span.innerHTML = innerHTML || '';
+    container.appendChild(span);
+    return span;
+};
+
+yoob.makeDiv = function(container, innerHTML) {
+    var div = document.createElement('div');
+    div.innerHTML = innerHTML || '';
+    container.appendChild(div);
+    return div;
+};
+
+yoob.makePre = function(container, innerHTML) {
+    var elem = document.createElement('pre');
+    elem.innerHTML = innerHTML || '';
+    container.appendChild(elem);
+    return elem;
+};
+
+yoob.makePanel = function(container, title, isOpen) {
+    isOpen = !!isOpen;
+    var panelContainer = document.createElement('div');
+    var button = document.createElement('button');
+    var innerContainer = document.createElement('div');
+    innerContainer.style.display = isOpen ? "block" : "none";
+
+    button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title;
+    button.onclick = function(e) {
+        isOpen = !isOpen;
+        button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title;
+        innerContainer.style.display = isOpen ? "block" : "none";
+    };
+
+    panelContainer.appendChild(button);
+    panelContainer.appendChild(innerContainer);
+    container.appendChild(panelContainer);
+    return innerContainer;
+};
+
+yoob.makeTextArea = function(container, cols, rows, initial) {
+    var textarea = document.createElement('textarea');
+    textarea.rows = "" + rows;
+    textarea.cols = "" + cols;
+    if (initial) {
+        container.value = initial;
+    }
+    container.appendChild(textarea);
+    return textarea;
+};
+
+yoob.makeLineBreak = function(container) {
+    var br = document.createElement('br');
+    container.appendChild(br);
+    return br;
+};
+
+yoob.makeSelect = function(container, labelText, optionsArray) {
+    var label = document.createElement('label');
+    label.innerHTML = labelText;
+    container.appendChild(label);
+
+    var select = document.createElement("select");
+
+    for (var i = 0; i < optionsArray.length; i++) {
+        var op = document.createElement("option");
+        op.value = optionsArray[i][0];
+        op.text = optionsArray[i][1];
+        if (optionsArray[i].length > 2) {
+            op.selected = optionsArray[i][2];
+        } else {
+            op.selected = false;
+        }
+        select.options.add(op);
+    }
+
+    container.appendChild(select);
+    return select;
+};
+
+SliderPlusTextInput = function() {
+    this.init = function(cfg) {
+        this.slider = cfg.slider;
+        this.textInput = cfg.textInput;
+        this.callback = cfg.callback;
+        return this;
+    };
+
+    this.set = function(value) {
+        this.slider.value = "" + value;
+        this.textInput.value = "" + value;
+        this.callback(value);
+    };
+};
+
+yoob.makeSliderPlusTextInput = function(container, label, min_, max_, size, value, fun) {
+    yoob.makeSpan(container, label);
+    var slider = yoob.makeSlider(container, min_, max_, value);
+    var s = "" + value;
+    var textInput = yoob.makeTextInput(container, size, s);
+    slider.onchange = function(e) {
+        textInput.value = slider.value;
+        fun(parseInt(slider.value, 10));
+    };
+    textInput.onchange = function(e) {
+        var v = parseInt(textInput.value, 10);
+        if (v !== NaN) {
+            slider.value = "" + v;
+            fun(v);
+        }
+    };
+    return new SliderPlusTextInput().init({
+        'slider': slider,
+        'textInput': textInput,
+        'callback': fun
+    });
+};
diff --git a/src/yoob/scanner.js b/src/yoob/scanner.js
new file mode 100644
index 0000000..f755d46
--- /dev/null
+++ b/src/yoob/scanner.js
@@ -0,0 +1,100 @@
+/*
+ * This file is part of yoob.js version 0.3
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * A lexical analyzer.
+ * Create a new yoob.Scanner object, then call init, passing it an
+ * array of two-element arrays; first element of each of these is the
+ * type of token, the second element is a regular expression (in a
+ * String) which matches that token at the start of the string.  The
+ * regular expression should have exactly one capturing group.
+ * Then call reset, passing it the string to be scanned.
+ * 
+ */
+yoob.Scanner = function() {
+  this.text = undefined;
+  this.token = undefined;
+  this.type = undefined;
+  this.error = undefined;
+  this.table = undefined;
+  this.whitespacePattern = "^[ \\t\\n\\r]*";
+
+  this.init = function(table) {
+    this.table = table;
+  };
+
+  this.reset = function(text) {
+    this.text = text;
+    this.token = undefined;
+    this.type = undefined;
+    this.error = undefined;
+    this.scan();
+  };
+  
+  this.scanPattern = function(pattern, type) {
+    var re = new RegExp(pattern);
+    var match = re.exec(this.text);
+    if (match === null) return false;
+    this.type = type;
+    this.token = match[1];
+    this.text = this.text.substr(match[0].length);
+    return true;
+  };
+
+  this.scan = function() {
+    this.scanPattern(this.whitespacePattern, "whitespace");
+    if (this.text.length === 0) {
+      this.token = null;
+      this.type = "EOF";
+      return;
+    }
+    for (var i = 0; i < this.table.length; i++) {
+      var type = this.table[i][0];
+      var pattern = this.table[i][1];
+      if (this.scanPattern(pattern, type)) return;
+    }
+    if (this.scanPattern("^([\\s\\S])", "unknown character")) return;
+    // should never get here
+  };
+
+  this.expect = function(token) {
+    if (this.token === token) {
+      this.scan();
+    } else {
+      this.error = "expected '" + token + "' but found '" + this.token + "'";
+    }
+  };
+
+  this.on = function(token) {
+    return this.token === token;
+  };
+
+  this.onType = function(type) {
+    return this.type === type;
+  };
+
+  this.checkType = function(type) {
+    if (this.type !== type) {
+      this.error = "expected " + type + " but found " + this.type + " (" + this.token + ")"
+    }
+  };
+
+  this.expectType = function(type) {
+    this.checkType(type);
+    this.scan();
+  };
+
+  this.consume = function(token) {
+    if (this.on(token)) {
+      this.scan();
+      return true;
+    } else {
+      return false;
+    }
+  };
+
+};