Need help with choo?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

6.6K Stars 624 Forks MIT License 575 Commits 42 Opened issues


:steam_locomotive::train: - sturdy 4kb frontend framework

Services available


Need anything else?

Contributors list


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

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

Table of Contents


  • minimal size: weighing
    , 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
  • isomorphic: renders seamlessly in both Node and browsers
  • very cute: choo choo!


var html = require('choo/html')
var devtools = require('choo-devtools')
var choo = require('choo')

var app = choo() app.use(devtools()) app.use(countStore) app.route('/', mainView) app.mount('body')

function mainView (state, emit) { return html`

  <h1>count is ${state.count}</h1>
  <button onclick="${onclick}">Increment</button>


function onclick () { emit('increment', 1) } }

function countStore (state, emitter) { state.count = 0 emitter.on('increment', function (count) { state.count += count emitter.emit('render') }) }

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.


At the core of Choo is an event emitter, which is used for both application logic but also to interface with the framework itself. The package we use for this is nanobus.

You can access the emitter through

app.use(state, emitter, app)
view(state, emit))
. Routes only have access to the
method to encourage people to separate business logic from render logic.

The purpose of the emitter is two-fold: it allows wiring up application code together, and splitting it off nicely - but it also allows communicating with the Choo framework itself. All events can be read as constants from
. Choo ships with the following events built in:


Choo emits this when the DOM is ready. Similar to the DOM's

event, except it will be emitted even if the listener is added after the DOM became ready. Uses document-ready under the hood.


This event should be emitted to re-render the DOM. A common pattern is to update the

object, and then emit the
event straight after. Note that
will only have an effect once the
event has been fired.


Choo emits this event whenever routes change. This is triggered by either



This event should be emitted to navigate to a new route. The new route is added to the browser's history stack, and will emit

. Similar to history.pushState.


This event should be emitted to navigate to a new route. The new route replaces the current entry in the browser's history stack, and will emit

. Similar to history.replaceState.


This event is emitted when the user hits the 'back' button in their browser. The new route will be a previous entry in the browser's history stack, and immediately afterward the

events will be emitted. Similar to history.popState. (Note that
will not cause a popState action - use
for that - this is different from the behaviour of


This event should be emitted whenever the

needs to be updated. It will set both
. This value can be used when server rendering to accurately include a
 tag in the header.
This is derived from the
DOMTitleChanged event.


Choo comes with a shared state object. This object can be mutated freely, and is passed into the view functions whenever

is emitted. The state object comes with a few properties set.

When initializing the application,

is used to provision the initial state. This is especially useful when combined with server rendering. See server rendering for more details.

A mapping of Choo's built in events. It's recommended to extend this object with your application's events. By defining your event names once and setting them on
, it reduces the chance of typos, generally autocompletes better, makes refactoring easier and compresses better.


The current params taken from the route. E.g.

becomes available as
If a wildcard route is used (
) it's available as


An object containing the current queryString.

{ bin:
'baz' }


An object containing the current href.



The current name of the route used in the router (e.g.



The current page title. Can be set using the



An object recommended to use for local component state.

state.cache(Component, id, [...args])

Generic class cache. Will lookup Component instance by id and create one if not found. Useful for working with stateful components.


Choo is an application level framework. This means that it takes care of everything related to routing and pathnames for you.


Params can be registered by prepending the route name with

, e.g.
. The value of the param will be saved on
). Wildcard routes can be registered with
, e.g.
. The value of the wildcard will be saved under

Default routes

Sometimes a route doesn't match, and you want to display a page to handle it. You can do this by declaring

app.route('*', handler)
to handle all routes that didn't match anything else.


Querystrings (e.g.

) are ignored when matching routes. An object containing the key-value mappings exists as

Hash routing

By default, hashes are ignored when routing. When enabling hash routing (

choo({ hash: true })
) hashes will be treated as part of the url, converting
. This is useful if the application is not mounted at the website root. Unless hash routing is enabled, if a hash is found we check if there's an anchor on the same page, and will scroll the element into view. Using both hashes in URLs and anchor links on the page is generally not recommended.

Following links

By default all clicks on

tags are handled by the router through the nanohref module. This can be disabled application-wide by passing
{ href: false }
to the application constructor. The event is not handled under the following conditions: - the click event had
called on it - the link has a
attribute with
rel="noopener noreferrer"
- a modifier key is enabled (e.g.
) - the link's href starts with protocol handler such as
- the link points to a different host - the link has a

:warn: Note that we only handle

if they also have
rel="noopener noreferrer"
on them. This is needed to properly sandbox web pages.

Navigating programmatically

To navigate routes you can emit

. See #events for more details about these events.

Server Rendering

Choo was built with Node in mind. To render on the server call

.toString(route, [state])
on your
var html = require('choo/html')
var choo = require('choo')

var app = choo() app.route('/', function (state, emit) { return html<div>Hello ${}</div> })

var state = { name: 'Node' } var string = app.toString('/', state)

console.log(string) // => '

Hello Node

When starting an application in the browser, it's recommended to provide the same

object available as
. When the application is started, it'll be used to initialize the application state. The process of server rendering, and providing an initial state on the client to create the exact same document is also known as "rehydration".

For security purposes, after

is used it is deleted from the
<script>window.initialState = { initial: 'state' }</script>


From time to time there will arise a need to have an element in an application hold a self-contained state or to not rerender when the application does. This is common when using 3rd party libraries to e.g. display an interactive map or a graph and you rely on this 3rd party library to handle modifications to the DOM. Components come baked in to Choo for these kinds of situations. See nanocomponent for documentation on the component class.

// map.js
var html = require('choo/html')
var mapboxgl = require('mapbox-gl')
var Component = require('choo/component')

module.exports = class Map extends Component { constructor (id, state, emit) { super(id) this.local = state.components[id] = {} }

load (element) { = new mapboxgl.Map({ container: element, center: }) }

update (center) { if (center.join() !== { } return false }

createElement (center) { = center return html<div></div> } }

// index.js
var choo = require('choo')
var html = require('choo/html')
var Map = require('./map.js')

var app = choo() app.route('/', mainView) app.mount('body')

function mainView (state, emit) { return html`

  <button onclick="${onclick}">Where am i?</button>
  ${state.cache(Map, 'my-map').render(}


function onclick () { emit('locate') } }

app.use(function (state, emitter) { = [18.0704503, 59.3244897] emitter.on('locate', function () { window.navigator.geolocation.getCurrentPosition(function (position) { = [position.coords.longitude, position.coords.latitude] emitter.emit('render') }) }) })

Caching components

When working with stateful components, one will need to keep track of component instances –

does just that. The component cache is a function which takes a component class and a unique id (
) as its first two arguments. Any following arguments will be forwarded to the component constructor together with

The default class cache is an LRU cache (using nanolru), meaning it will only hold on to a fixed amount of class instances (

by default) before starting to evict the least-recently-used instances. This behavior can be overriden with options.


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.

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

evaluates the
method on nodes to determine if they should be updated or not.
var el = html`

// tell nanomorph to not compare the DOM tree if they're both divs el.isSameNode = function (target) { return (target && target.nodeName && target.nodeName === 'DIV') }

Reordering lists

It's common to work with lists of elements on the DOM. Adding, removing or reordering elements in a list can be rather expensive. To optimize this you can add an

attribute to a DOM node. When reordering nodes it will compare nodes with the same ID against each other, resulting in far fewer re-renders. This is especially potent when coupled with DOM node caching.
var el = html`

Pruning dependencies

We use the

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

. This has overhead during runtime, so for production environments we should unwrap this using the nanohtml transform.

Setting up browserify transforms can sometimes be a bit of hassle; to make this more convenient we recommend using bankai build to build 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. :steam_locomotive::train::train::train:

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

with nanohtml as a global transform or using bankai directly.
$ browserify -g nanohtml

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

app = choo([opts])

Initialize a new

can also contain the following values: - opts.history: default:
. Listen for url changes through the history API. - opts.href: default:
. Handle all relative
clicks and call
- opts.cache: default:
. Override default class cache used by
. Can be a a
(maximum number of instances in cache, default
) or an
with a nanolru-compatible API. - opts.hash: default:
. Treat hashes in URLs as part of the pathname, transforming
. This is useful if the application is not mounted at the website root.

app.use(callback(state, emitter, app))

Call a function and pass it a

is an instance of nanobus. You can listen to messages by calling
and emit messages by calling
is the same Choo instance. Callbacks passed to
are commonly referred to as

If the callback has a

property on it, it will be used to identify the callback during tracing.

See #events for an overview of all events.

app.route(routeName, handler(state, emit))

Register a route on the router. The handler function is passed

as arguments. Uses nanorouter under the hood.

See #routing for an overview of how to use routing efficiently.


Start the application and mount it on the given

, the given selector can be a String or a DOM element.

In the browser, this will replace the selector provided with the tree returned from

. If you want to add the app as a child to an element, use
to obtain the tree and manually append it.

On the server, this will save the

on the app instance. When doing server side rendering, you can then check the
property to see where the render result should be inserted.


, so you can easily export the application for server side rendering:
module.exports = app.mount('body')

tree = app.start()

Start the application. Returns a tree of DOM nodes that can be mounted using


app.toString(location, [state])

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


Create DOM nodes from template string literals. Exposes nanohtml. Can be optimized using nanohtml.


Exposes nanohtml/raw helper for rendering raw HTML content.


$ npm install choo

See Also

  • bankai - streaming asset compiler
  • - open software ecosystem for WebGL
  • yo-yo - tiny library for modular UI
  • tachyons - functional CSS for humans
  • sheetify - modular CSS bundler for


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



We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.