A framework for building full-stack web apps in Roc!
Joy is a jolt of happiness for those who want a fast, productive, fun, and statically-typed environment for full-stack development.
Joy should provide:
- A convenient way of writing full-stack apps all in Roc (sometimes called "isomorphic" apps).
- A convenient way of communicating between the front- and back-end (think Server Functions or a protocol for message-passing over Server-Sent Events, websockets, and/or plain old requests).
- Great developer experience (feedback, iteration time, tooling, etc.)
- Great performance for the vast majority of apps, but not at any cost.
Joy is fun to play with, but it's in early development, not production-ready. Here be dragons!
You can already build single-page applications in it but the functionality is limited. Have a look at the examples to get a grasp on what's currently supported. See Joy TodoMVC for a complete front-end example.
A full-stack example will follow! In the meantime you can use roc-lang/basic-webserver and joy-html to serve Joy HTML from the back-end.
Have fun and expect breaking changes!
A client-side counter:
app [Model, Msg, init, update, render, subscriptions] {
pf: platform "https://github.com/niclas-ahden/joy/releases/download/0.32.1/BBEFdA1VAk1WZvQWKs3yNfN2RMdZZP5DsM8j7gyNoFta.tar.zst",
html: "https://github.com/niclas-ahden/joy-html/releases/download/0.15.0/5Yoz712P8ed4MBW74eddTEJdZ92ZDCUbVGFkt4XXSuj9.tar.zst",
}
import html.Html exposing [Html, div, button, text]
import html.Attribute exposing [on_click]
import pf.Effect exposing [Effect]
Model : { count : I64 }
Msg : [Increment, Decrement]
subscriptions = |_model| []
init : Str -> (Model, List(Effect(Msg)))
init = |_| ({ count: 0 }, [])
update : Model, Msg -> (Model, List(Effect(Msg)))
update = |model, msg|
match msg {
Increment => ({ count: model.count + 1 }, [])
Decrement => ({ count: model.count - 1 }, [])
}
render : Model -> Html(Msg)
render = |model|
div(
[],
[
button([on_click(Increment)], [text("+")]),
text(model.count.to_str()),
button([on_click(Decrement)], [text("-")]),
],
)Start with the Joy TodoMVC example, and you'll have a complete front-end app setup in no time!
Joy is tracked against the js-framework-benchmark, with keyed and non-keyed entries. Numbers below are from a local run and give you a rough idea of our relative performance:
| Joy (keyed) | Joy (non-keyed) | Elm (keyed) | Elm (non-keyed) | Leptos | SolidJS | React | vanilla JS | |
|---|---|---|---|---|---|---|---|---|
| CPU geomean (ms) | 29.6 | 26.7 | 31.7 | 29.0 | 29.0 | 26.0 | 40.7 | 22.4 |
| Slowdown vs best | 1.42× | 1.28× | 1.52× | 1.39× | 1.39× | 1.24× | 1.95× | 1.07× |
| Memory (MB) | 3.6 | 3.6 | 1.3 | 1.3 | 3.5 | 1.0 | 2.2 | 0.8 |
| Bundle, compressed (KB) | 27.5 | 27.3 | 7.9 | 8.2 | 48.8 | 4.5 | 51.4 | 2.4 |
Bundle is the brotli-compressed transfer of everything the page loads, so for Joy that covers the WASM, the JS runtime, and the HTML. The WASM is the bulk of it at 22.1 KB, and the minified JS runtime is 5.0 KB.
Every framework in the table was measured in one session on a machine running NixOS, x86, AMD 9950X. Hopefully we can get Joy into the official benchmark soon!
Contributions are very welcome, including feature requests, design discussion, etc.
Clone the repo and use the included Nix flake to set up your development environment:
$ nix develop # Oh, lord, have mercy! This is great!If you don't want to use Nix then please install:
roc nightly-2026-09-04-c125b82rustc(v1.94 +wasm32-unknown-unknown)node(v22)watchexecplaywrightwith a chromium, for the browser E2E suite (npm install -g playwright && playwright install chromium)
Pick an example and run it like so:
$ ./watch.roc examples/hello.rocThe application should now be available at: http://localhost:8000
That builds at --opt=speed, the default. To pick another level, put roc's -- separator in front of the flag so roc passes it on to the script instead of claiming it: ./watch.roc -- --opt=dev examples/hello.roc.
Start modifying the example to get a feel for it. Refresh the browser to see your changes (the app is recompiled on change but there's no browser hot-reloading yet).
$ ./tests.roc -- --opt=speed # Roc unit tests + the fake-DOM harnesses in tests/
$ ./e2e.roc -- --opt=speed # tests/e2e/ in a real headless ChromiumThe harnesses in tests/ mount the built apps on a fake DOM and cover the app and runtime logic. The browser suite in tests/e2e/ is built on roc-spec and roc-playwright and covers what only a real browser can prove: real event dispatch and bubbling, <dialog> semantics, the History API, WebCrypto, fetch, real timers, and real keyboard and mouse input.
Both suites take the optimization level to test as their argument, and it is required here, so ./tests.roc -- --opt=dev and ./e2e.roc -- --opt=dev run the same suites against dev builds. Each level lands in its own build/<opt>/ tree.
Joy is sponsored by the real estate agency BOSTHLM Fastighetsmäklare which thrives by using technology to bolster its agents and business. Thank you!
Have a look at their search feature on www.bosthlm.se/till-salu which is a front-end application written in Roc.