Twine: Javascript passage transition "Typewriter"

While CSS passage transitions give you a good degree of creativity with regards to transition animations, a good many potential effects require Javascript. One such effect is the oft vaunted "typewriter" transition:

(function(){postrender.typewriter = function (b) {
if(this.tags){var r=new RegExp("t8n.typewriter.([0-9]+)(?:[^0-9]|$)","g");var t=r.exec(this.tags.toString());
if(t){typeout(b,t[1]+0);}}return b;};var typeout=function(c,t){var Furl=function(current){this.n=current;
this.out=false;;current.nodeValue="";[];var cn=current.childNodes;
if( &&"none"){return;}while(cn.length>0){var f=new Furl(cn[0]);
current.removeChild(cn[0]);f.out=true;;}};var nodes=new Furl(c);
var unfurl=function(furled,d){var n=furled.n;if(furled.out){d.appendChild(n);furled.out=false;
}if({[0];;return true;
}for(var j=0;j<;j++){var ret=unfurl([j],n);if(ret){return true;
}}return false;};var title=state.history[0].passage.title;var intr=setInterval(function(){if(state.history[0].passage.title==title&&unfurl(nodes,null)){return;

To use it for a passage, tag the passage with "t8n-typewriter-" followed by the number of milliseconds between each character appearing - for instance "t8n-typewriter-10" or "t8n-typewriter-2". If you have a lot of text, a low number is highly recommended.

("t8n" is a numeronym for the word "transition". For this and any future transitions of mine, "t8n" will denote a tag that invokes that transition.)

This can be combined with CSS transitions! See this example to see a combination of the two.


  1. Images amid the text will currently appear instantly, as if they were single characters. This may appear a little jarring if they are below text.
  2. This transition currently does not apply to the <<replace>> macro - replaced text will simply appear normally.
  3. This transition currently may interfere with the timing of the <<timedreplace>> macro. If the <<timedreplace>> macro triggers before it has been fully typed out, then it will not function correctly. Simply make sure that the <<timedreplace>> is timed to trigger only after it appears.

These caveats may be improved in future versions. Other possible improvements may include a macro designed to alter typing speed within the text itself, so that the full range of pauses, staccato, blurt-outs, etc. as seen in various RPGs, can be replicated in Twine.

Version history:

  1. 22-3-13: Initial.

Feel free to report any bugs to @webbedspace.

tsitr_typeout1.html95.35 KB
tsitr_decrypt.html95.81 KB
tsitr_corrupted.html96.59 KB


Postrender error & example code

Thanks a lot for this code. I am getting a postrender error on Twine 2 & Harlowe even if I just add the JS. Any way you can share the example as an archive so we can peek under the wizard's skirt (metaphorically speaking)?
Thank you!

sergiocornaga's picture

Leon's code snippets here

Leon's code snippets here are mostly for Twine 1 rather than Twine 2. Looking around a little, I found some potential fixes here and here.

cammySashimi's picture

HEY, I know it's been over a

HEY, I know it's been almost a year since you posted this, but I modified the JS to work on Twine 2 (at least on Sugarcube 1.x) because I was frustrated with some of the limitations of typed.js and I always thought this was a cleaner implementation anyways. So for anyone who needs it in the future:

(Since this comment section messes with the formatting, I also made a pastebin here:

/* typewriter */ !function() { postrender.typewriter = function (b) { if (this.tags) { var r = new RegExp("t8n.typewriter.([0-9]+)(?:[^0-9]|$)","g"); var t = r.exec(this.tags.toString()); if (t) { typeout(b, t[1]+0); } } return b; }; var typeout = function(c,t) { var Furl = function(current) { this.n = current; this.out = false; = current.nodeValue; current.nodeValue = ""; = []; var cn = current.childNodes; if ( &&"none") { return; } while (cn.length>0) { var f = new Furl(cn[0]); current.removeChild(cn[0]); f.out = true;; } }; var nodes = new Furl(c); var unfurl = function(furled,d) { var n = furled.n; if (furled.out) { d.appendChild(n); furled.out = false; } if ( { n.nodeValue +=[0]; =; return true; } for (var j=0; j