Arizona
Arizona is a real-time web framework for Erlang/OTP. It renders HTML on the server, diffs changes at the template level, and pushes minimal updates to the browser over WebSocket.
Templates are plain Erlang terms compiled via parse transform. The server owns the state; the client is a thin DOM patcher.
🚧 Status
Arizona is in 0.x. The core is functional and covered by tests, but the API may change between
minor versions. Pin an exact version in your deps (e.g. {arizona, "0.1.0"}) if you need stability
across upgrades.
Features
- SSR + live updates -- HTML on first load, WebSocket diffs after
- Erlang-native templates --
{Tag, Attrs, Children}tuples compiled by parse transform - Compile-time static/dynamic split -- statics sent once, only dynamics cross the wire
- Three handler kinds --
arizona_view(route pages),arizona_stateful(components),arizona_stateless(pure templates) - Streams -- keyed collections with insert/delete/update/move/sort/limit
- SPA navigation --
az_navigatelinks, server renders the next page over WebSocket - PubSub -- cross-view, cross-tab messaging via
arizona_pubsub - Route middlewares -- gate or rewrite requests before mount (auth, sessions, URL projection)
- On-mount hooks -- per-route pipeline that runs before every mount, including navigate
- Element hooks -- client-side
mounted/updated/destroyedcallbacks viaaz_hook - Dev-mode hot reload --
fswatcher recompiles changed.erlfiles and pushes reload events - Pluggable transport -- cowboy adapter built-in; write your own to swap it out
Requirements
- Erlang/OTP 28 or later
Installation
Add Arizona to your rebar.config dependencies. Cowboy is required for the built-in
HTTP/WebSocket transport; skip it only if you write your own arizona_req adapter.
{deps, [
{arizona, "~> 0.1"},
cowboy
]}.To track unreleased changes, swap the version for a git ref:
{arizona, {git, "https://github.com/arizona-framework/arizona.git", {branch, "main"}}}
The client JavaScript ships baked into the rebar3 build (priv/static/assets/js/*.min.js). If
you need to bundle it yourself or consume it from a non-Erlang backend, install via npm:
npm install @arizona-framework/clientimport { connect } from '@arizona-framework/client';
connect('/ws');Quick start
A page with an embedded counter.
1. The counter component
id and initial count come from the parent via ?stateful (step 2), so mount/1 just passes
them through:
%% src/my_counter.erl
-module(my_counter).
-include_lib("arizona/include/arizona_stateful.hrl").
-export([mount/1, render/1, handle_event/3]).
mount(Bindings) ->
{Bindings, #{}}.
render(Bindings) ->
?html(
%% id must be on the root element -- if it changes, the component is remounted
{'div', [{id, ?get(id)}], [
{button, [{az_click, arizona_js:push_event(~"dec")}], [~"-"]},
{span, [], [~" Count: ", ?get(count), ~" "]},
{button, [{az_click, arizona_js:push_event(~"inc")}], [~"+"]}
]}
).
handle_event(~"inc", _Payload, Bindings) ->
{Bindings#{count => maps:get(count, Bindings) + 1}, #{}, []};
handle_event(~"dec", _Payload, Bindings) ->
{Bindings#{count => maps:get(count, Bindings) - 1}, #{}, []}.?get(count) registers count as a dependency of that template slot. When handle_event returns
new bindings, only slots whose tracked keys changed re-render -- the <span> patches; the buttons
don't.
The tuples carry more than just bindings: mount/1 returns {Bindings, Resets} (an explicit
slot-reset map -- usually #{}), and handle_event/3 returns {Bindings, Resets, Effects} where
Effects is a list of arizona_js commands (set_title, navigate, …) executed on the client.
2. The parent page
A view is the route's root handler. It receives initial bindings plus the request:
%% src/my_page.erl
-module(my_page).
-include_lib("arizona/include/arizona_view.hrl").
-export([mount/2, render/1]).
mount(Bindings, _Req) ->
{Bindings, #{}}.
render(Bindings) ->
?html(
{main, [{id, ?get(id)}], [
{h1, [], [~"Counter demo"]},
%% id is required -- it's how the diff engine routes patches to this component
?stateful(my_counter, #{id => ~"counter", count => 0})
]}
).3. The layout
The HTML shell. Loads the client runtime that connects over WebSocket:
%% src/my_layout.erl
-module(my_layout).
-include_lib("arizona/include/arizona_stateless.hrl").
-export([render/1]).
render(Bindings) ->
?html([
~"<!DOCTYPE html>",
{html, [], [
{head, [], [
{meta, [{charset, ~"utf-8"}]},
{title, [], [?get(title, ~"Arizona")]}
]},
{body, [], [
?inner_content,
{script, [{type, ~"module"}], [
~"""
import { connect } from '/assets/arizona.min.js';
connect('/ws');
"""
]}
]}
]}
]).4. Configure the server
Add arizona to your app's applications list in .app.src:
{applications, [kernel, stdlib, cowboy, arizona]}
Then declare routes in config/sys.config:
[{arizona, [
{server, #{
routes => [
{live, ~"/", my_page, #{
layouts => [{my_layout, render}],
bindings => #{id => ~"page", title => ~"Counter demo"}
}},
{ws, ~"/ws", #{}},
{asset, ~"/assets", {priv_dir, arizona, "static/assets/js"}}
]
}}
]}].5. Run it
rebar3 shellOpen http://localhost:4040 and click the buttons -- the server renders the initial HTML, then pushes minimal diffs over WebSocket as the count changes.
Documentation
See docs/architecture.md for the full architecture reference -- module breakdown, op codes, dev-mode file watchers, custom schemes/proto_opts, and imperative startup.
Sponsors
If you like Arizona, please consider sponsoring me. I'm thankful for your never-ending support ❤️
I also accept coffees ☕
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for development setup, testing guidelines, and contribution workflow.
Contributors
<img src="https://contrib.rocks/image?repo=arizona-framework/arizona&max=100&columns=10" width="15%" alt="Contributors" />Star History
<picture> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=arizona-framework/arizona&type=Date&theme=dark" /> <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=arizona-framework/arizona&type=Date" /> <img src="https://api.star-history.com/svg?repos=arizona-framework/arizona&type=Date" alt="Star History Chart" width="100%" /> </picture>License
Copyright (c) 2023-2026 William Fank Thomé
Arizona is open-source under the Apache 2.0 License on GitHub.
See LICENSE.md for more information.