node package manager



Fun functional programming
A 4kb framework for creating sturdy frontend applications

API stability NPM version Build Status Test Coverage Downloads Standard

Website | Handbook | Ecosystem | Contributing | Chat

The little framework that could. Built with ❤︎ by Yoshua Wuyts and contributors

Table of Content


  • minimal size: weighing 4kb, choo is a tiny little framework
  • event based: our performant event system makes writing apps easy
  • small api: with only 6 methods there's not much to learn
  • minimal tooling: built for the cutting edge browserify compiler
  • isomorphic: renders seamlessly in both Node and browsers
  • very cute: choo choo!


var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.route('/', mainView)
function mainView (state, emit) {
  return html`
      <h1>count is ${state.count}</h1>
      <button onclick=${onclick}>Increment</button>
  function onclick () {
    emit('increment', 1)
function logger (state, emitter) {
  emitter.on('*', function (messageName, data) {
    console.log('event', messageName, data)
function countStore (state, emitter) {
  state.count = 0
  emitter.on('increment', function (count) {
    state.count += count

Want to see more examples? Check out the Choo handbook.


We believe programming should be fun and light, not stern and stressful. It's cool to be cute; using serious words without explaining them doesn't make for better results - if anything it scares people off. We don't want to be scary, we want to be nice and fun, and then casually be the best choice around. Real casually.

We believe frameworks should be disposable, and components recyclable. We don't want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. choo is modest in its design; we don't believe it will be top of the class forever, so we've made it as easy to toss out as it is to pick up.

We don't believe that bigger is better. Big APIs, large complexities, long files - we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.


Choo is reasonably fast out of the box. But sometimes you might hit a scenario where a particular part of the UI slows down the application, and you want to speed it up. Here are some optimizations that are possible.

Reordering lists

To be implemented. (See yoshuawuyts/nanomorph#8)

Caching DOM elements

Sometimes we want to tell the algorithm to not evaluate certain nodes (and its children). This can be because we're sure they haven't changed, or perhaps because another piece of code is managing that part of the DOM tree. To achieve this nanomorph evaluates the .isSameNode() method on nodes to determine if they should be updated or not.

var el = html`<div>node</div>`
// tell nanomorph to not compare the DOM tree if they're both divs 
el.isSameNode = function (target) {
  return (target && target.nodeName && target.nodeName === 'DIV')

Pruning dependencies

We use the require('assert') module from Node core to provide helpful error messages in development. In production you probably want to strip this using unassertify.

To convert inlined HTML to valid DOM nodes we use require('bel'). This has overhead during runtime, so for production environments we should unwrap this using yo-yoify.

Setting up browserify transforms can sometimes be a bit of hassle; to make this more convenient we recommend using bankai with --optimize to compile your assets for production.


Why is it called choo?

Because I thought it sounded cute. All these programs talk about being "performant", "rigid", "robust" - I like programming to be light, fun and non-scary. choo embraces that.

Also imagine telling some business people you chose to rewrite something critical for serious bizcorp using a train themed framework. 🚂🚋🚋🚋

Is it called choo, choo.js or...?

It's called "choo", though we're fine if you call it "choo-choo" or "chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if / when you shimmy like you're a locomotive.

Does choo use a virtual-dom?

choo uses nanomorph, which diffs real DOM nodes instead of virtual nodes. It turns out that browsers are actually ridiculously good at dealing with DOM nodes, and it has the added benefit of working with any library that produces valid DOM nodes. So to put a long answer short: we're using something even better.

How can I support older browsers?

Template strings aren't supported in all browsers, and parsing them creates significant overhead. To optimize we recommend running browserify with yo-yoify as a global transform or using bankai directly.

$ browserify -g yo-yoify

Is choo production ready?



This section provides documentation on how each function in choo works. It's intended to be a technical reference. If you're interested in learning choo for the first time, consider reading through the handbook first ✨

app = choo([opts])

Initialize a new choo instance. opts can also contain the following values:

  • opts.history: default: true. Listen for url changes through the history API.
  • opts.href: default: true. Handle all relative <a href="<location>"></a> clicks and call emit('render')
  • opts.timing: default: true. Enables calls to the window.performance timing API. Timing calls will not run in browsers that don't support it out of the box. The timing marks are choo:renderStart, choo:renderEnd. The resulting diff is stored as choo:render.

app.use(callback(state, emitter))

Call a function and pass it a state and emitter. emitter is an instance of nanobus. You can listen to messages by calling emitter.on() and emit messages by calling emitter.emit().

Choo fires messages when certain events happen:

  • .on('DOMContentLoaded'): when the DOM has succesfully finished loading
  • .on('render'): when the DOM re-renders
  • .on('pushState'): when the history API is triggered

The render event should be emitted (emitter.emit('render')) whenever you want the app to re-render the DOM - it won't happen on its own except when you navigate between routes.

The pushState can be emitted to navigate between routes: emitted.emit('pushState', '/some/route'). You can emit replaceState which will overwrite the current entry in the browser history, but be very careful as this removes the option of navigating back!

Note render will only have an effect once the DOMContentLoaded event has been fired.

app.route(routeName, handler)

Register a route on the router. Uses nanorouter under the hood. Params can be registered by prepending the routename with :routename, e.g. /foo/:bar/:baz. The value of the param will be saved on state.params (e.g. Wildcard routes can be registered with *, e.g. /foo/*. The value of the wildcard will be saved under state.params.wildcard.

Using hashes to delimit routes is supported out of the box (e.g. /foo#bar). When a hash is found we also check if there's an available anchor on the same page, and will scroll the screen to the position. Using both hashes in URLs and anchor links on the page is generally not recommended.

New routes can be triggered through emitter.emit('pushState', <routename>). By default we also catch and match all <a href=""> clicks against the router. This can be disabled by setting opts.href to false in the constructor. Routing via pushState will not work until the DOMContentLoaded event has been fired.

If you need choo to ignore a particular route, you can add data-no-routing attribute with <a href="" data-no-routing>. This is especially useful for directing outside the choo app.

Querystrings (?foo=bar) are ignored when matching routes. They should be extracted from the window.location object on render events, from either a custom event listener or the matched views.


Start the application and mount it on the given querySelector. Uses nanomount under the hood. This will replace the selector provided with the tree returned from app.start(). If you want to add the app as a child to an element, use app.start() to obtain the tree and manually append it.

tree = app.start()

Start the application. Returns a tree of DOM nodes that can be mounted using document.body.appendChild().

app.toString(location, [state])

Render the application to a string. Useful for rendering on the server.


Create DOM nodes from template string literals. Exposes bel. Can be optimized using yo-yoify.


$ npm install choo

See Also

  • bankai - streaming asset compiler
  • - open software ecosystem for WebGL
  • yo-yo - tiny library for modular UI
  • bel - composable DOM elements using template strings
  • tachyons - functional CSS for humans
  • sheetify - modular CSS bundler for browserify


Creating a quality framework takes a lot of time. Unlike others frameworks, Choo is completely independently funded. We fight for our users. This does mean however that we also have to spend time working contracts to pay the bills. This is where you can help: by chipping in you can ensure more time is spent improving Choo rather than dealing with distractions.


Become a sponsor and help ensure the development of independent quality software. You can help us keep the lights on, bellies full and work days sharp and focused on improving the state of the web. Become a sponsor


Become a backer, and buy us a coffee (or perhaps lunch?) every month or so. Become a backer