Components

Live views render entire pages. Components extract reusable pieces — a comment form, a notification badge, a tag selector. Arizona has two types: stateful components (with their own state and event handlers) and stateless components (pure render functions).

Stateless components

A stateless component is a function that takes bindings and returns a template. It has no state and no event handling — just rendering.

-module(blog_components).
-compile({parse_transform, arizona_parse_transform}).
-export([post_card/1, tag_badge/1, user_avatar/1]).

post_card(Bindings) ->
    Post = maps:get(post, Bindings),
    arizona_template:from_html(~"""
    <article class="post-card">
        <h2>{maps:get(title, Post)}</h2>
        <p class="meta">by {maps:get(username, maps:get(author, Post))}</p>
        <p>{binary:part(maps:get(body, Post), 0, min(200, byte_size(maps:get(body, Post))))}...</p>
    </article>
    """).

tag_badge(Bindings) ->
    Tag = maps:get(tag, Bindings),
    arizona_template:from_html(~"""
    <span class="tag">{maps:get(name, Tag)}</span>
    """).

user_avatar(Bindings) ->
    User = maps:get(user, Bindings),
    arizona_template:from_html(~"""
    <div class="avatar">
        <span>{binary:part(maps:get(username, User), 0, 1)}</span>
    </div>
    """).

Use stateless components in a live view with arizona_template:render_stateless/3:

render(Bindings) ->
    arizona_template:from_html(~"""
    <div class="post-list">
        {arizona_template:render_list(
            arizona_template:get_binding(posts, Bindings),
            fun(P) ->
                arizona_template:render_stateless(blog_components, post_card, #{post => P})
            end)}
    </div>
    """).

Stateful components

A stateful component has its own state, handles its own events, and re-renders independently of its parent. Each stateful component must have a unique id binding.

-module(blog_comment_form).
-compile({parse_transform, arizona_parse_transform}).
-behaviour(arizona_stateful).

-export([mount/1, render/1, handle_event/3]).

mount(Bindings) ->
    arizona_stateful:new(?MODULE, #{
        id => maps:get(id, Bindings),
        post_id => maps:get(post_id, Bindings),
        body => <<>>,
        error => undefined
    }).

render(Bindings) ->
    arizona_template:from_html(~"""
    <div id="comment-form">
        {case arizona_template:get_binding(error, Bindings) of
            undefined -> ~"";
            Err -> arizona_template:from_html(~"<p class='error'>{Err}</p>")
        end}
        <form az-submit="submit_comment">
            <textarea name="body" az-change="validate"
                placeholder="Write a comment...">{arizona_template:get_binding(body, Bindings)}</textarea>
            <button type="submit">Post Comment</button>
        </form>
    </div>
    """).

handle_event(<<"validate">>, #{<<"body">> := Body}, State) ->
    Error = case byte_size(Body) of
        0 -> <<"Comment cannot be empty">>;
        _ -> undefined
    end,
    S1 = arizona_stateful:put_binding(body, Body, State),
    S2 = arizona_stateful:put_binding(error, Error, S1),
    {[], S2};
handle_event(<<"submit_comment">>, #{<<"body">> := Body}, State) ->
    PostId = arizona_stateful:get_binding(post_id, State),
    CS = comment:changeset(#{}, #{<<"body">> => Body, <<"post_id">> => PostId}),
    case blog_repo:insert(CS) of
        {ok, _Comment} ->
            S1 = arizona_stateful:put_binding(body, <<>>, State),
            S2 = arizona_stateful:put_binding(error, undefined, S1),
            {[], S2};
        {error, _} ->
            S1 = arizona_stateful:put_binding(error, <<"Failed to post comment">>, State),
            {[], S1}
    end.

Embed a stateful component in a live view with arizona_template:render_stateful/2:

render(Bindings) ->
    Post = arizona_template:get_binding(post, Bindings),
    arizona_template:from_html(~"""
    <article>
        <h1>{maps:get(title, Post)}</h1>
        {arizona_template:render_stateful(blog_comment_form, #{
            id => ~"comment-form",
            post_id => maps:get(id, Post)
        })}
    </article>
    """).

The id is required — Arizona uses it to track the component instance across re-renders.

Slots

Slots let components accept nested content from their parent, enabling flexible composition:

-module(blog_card).
-compile({parse_transform, arizona_parse_transform}).
-export([render/1]).

render(Bindings) ->
    arizona_template:from_html(~"""
    <div class="card">
        <div class="card-header">
            <h3>{maps:get(title, Bindings)}</h3>
        </div>
        <div class="card-body">
            {arizona_template:render_slot(maps:get(inner_content, Bindings))}
        </div>
    </div>
    """).

When to use which

Use caseComponent type
Display-only UI pieces (badges, cards, avatars)Stateless
Interactive forms, toggles, dropdownsStateful
UI that needs its own event handlingStateful
Layout wrappers, formatting helpersStateless

Stateless components re-render when their parent re-renders. Stateful components re-render independently — only when their own state changes.


Next, let's handle user interactions in depth with Events & Interactivity.