Built for beauty and speed.

Cross-platform MIT-licensed Desktop GUI framework for C, C++, Python and Rust, using the Mozilla WebRender rendering engine

v0.2.0

2026-05-28

release notes api docs user guide
A minimal Azul application demonstrating the basic structure A minimal Azul application demonstrating the basic structure A minimal Azul application demonstrating the basic structure

Hello, World.
Goodbye, JavaScript.

No Chromium. No V8. No 200MB runtime. Just a 15MB DLL and GPU-accelerated rendering via WebRender. Your app state lives in YOUR code - Azul just renders it. Unlike React, you control exactly when the UI refreshes. Write once in Rust, C, C++ or Python. Style with real CSS. Ship a single binary that starts instantly.

more languages…
from azul import *


class DataModel:
    def __init__(self, counter):
        self.counter = counter


def layout(data, info):
    label = (Dom.create_text(str(data.counter))
             .with_css("font-size:50px;"))

    button = (Dom.create_div()
              .with_css("flex-grow:1;")
              .with_child(Dom.create_text("Increase counter"))
              .with_callback(
                  EventFilter.Hover(HoverEventFilter.MouseUp()),
                  data,
                  on_click))

    body = (Dom.create_body()
            .with_child(label)
            .with_child(button))

    return body.style(Css.empty())


def on_click(data, info):
    data.counter += 1
    return Update.RefreshDom()


model = DataModel(5)
window = WindowCreateOptions.create(layout)

app = App.create(model, AppConfig.create())
app.run(window)
Demonstration of all built-in widgets including buttons, checkboxes, inputs and more Demonstration of all built-in widgets including buttons, checkboxes, inputs and more Demonstration of all built-in widgets including buttons, checkboxes, inputs and more

Massive Widget
library

Buttons, inputs, dropdowns, tabs, color pickers, progress bars - all GPU-rendered and fully styleable via CSS. No DOM/JS bridge overhead. Callbacks are direct function pointers to your native code. Compose widgets freely - no prop drilling, no state lifting. Your architecture, your rules.

more languages…
# Widgets Showcase - Python
# python widgets.py

from azul import *


class WidgetShowcase:
    def __init__(self):
        self.enable_padding = True
        self.active_tab = 0
        self.progress_value = 25.0
        self.checkbox_checked = False
        self.text_input = ""


CLICK = EventFilter.Hover(HoverEventFilter.MouseUp())


def layout(data, info):
    title = (Dom.create_text("Widget Showcase")
             .with_css("font-size:24px;margin-bottom:20px;"))

    button = (Dom.create_div()
              .with_css("margin-bottom:10px;padding:10px;background:#4CAF50;"
                        "color:white;cursor:pointer;")
              .with_child(Dom.create_text("Click me!"))
              .with_callback(CLICK, data, on_button_click))

    checkbox = (CheckBox.create(data.checkbox_checked)
                .dom()
                .with_css("margin-bottom:10px;"))

    progress = (ProgressBar.create(data.progress_value)
                .dom()
                .with_css("margin-bottom:10px;"))

    text_input = (TextInput.create()
                  .with_placeholder("Enter text here...")
                  .dom()
                  .with_css("margin-bottom:10px;"))

    color_input = (ColorInput.create(ColorU(100, 150, 200, 255))
                   .dom()
                   .with_css("margin-bottom:10px;"))

    number_input = (NumberInput.create(42.0)
                    .dom()
                    .with_css("margin-bottom:10px;"))

    body = (Dom.create_body()
            .with_css("padding:20px;font-family:sans-serif;")
            .with_child(title)
            .with_child(button)
            .with_child(checkbox)
            .with_child(progress)
            .with_child(text_input)
            .with_child(color_input)
            .with_child(number_input))

    return body.style(Css.empty())


def on_button_click(data, info):
    data.progress_value += 10.0
    if data.progress_value > 100.0:
        data.progress_value = 0.0
    return Update.RefreshDom()


model = WidgetShowcase()
window = WindowCreateOptions.create(layout)
app = App.create(model, AppConfig.create())
app.run(window)
Hardware-accelerated custom rendering with OpenGL textures Hardware-accelerated custom rendering with OpenGL textures Hardware-accelerated custom rendering with OpenGL textures

OpenGL
Integration

Embed raw OpenGL directly in your UI - no WebGL abstraction, no canvas hacks. Render 3D scenes, CAD models, or data visualizations right next to native widgets. Perfect for scientific apps, GIS or CAD.

more languages…
# OpenGL Integration - Python
# python opengl.py
#
# NOTE: Texture-creation callbacks (RenderImageCallback) and timer setup
# both go through Py<PyAny> in the Python binding; the relevant glue is
# still in flight. The example renders a static splash for now.

from azul import *


class OpenGlState:
    def __init__(self):
        self.rotation_deg = 0.0
        self.texture_uploaded = False


def layout(data, info):
    title = (Dom.create_text("OpenGL Integration Demo")
             .with_css("color:white;font-size:24px;margin-bottom:20px;"))

    placeholder = (Dom.create_text(
        "OpenGL texture would render here (timer-driven animation pending)")
        .with_css("flex-grow:1;min-height:300px;border-radius:10px;"
                  "background:#222;color:white;display:flex;"
                  "align-items:center;justify-content:center;"
                  "box-shadow:0px 0px 20px rgba(0,0,0,0.5);"))

    body = (Dom.create_body()
            .with_css("background:linear-gradient(#1a1a2e, #16213e);padding:20px;")
            .with_child(title)
            .with_child(placeholder))

    return body.style(Css.empty())


state = OpenGlState()
window = WindowCreateOptions.create(layout)
app = App.create(state, AppConfig.create())
app.run(window)
Loading and displaying infinite content using VirtualViewCallbacks Loading and displaying infinite content using VirtualViewCallbacks Loading and displaying infinite content using VirtualViewCallbacks

Infinite
Scrolling

Display 10 million rows at 60 FPS. VirtualViewCallbacks render only what's visible on screen. Lazy-load images, SVGs, and complex content as users scroll - zero upfront memory cost. Perfect for IDE file trees, database viewers, and infinite feeds, ...

more languages…
# Infinite Scrolling - Python
# python infinity.py
#
# NOTE: VirtualView requires custom callbacks plus an OptionDom return,
# both of which are still experimental in the Python binding. This example
# falls back to a plain scrolling container that pre-renders a windowed slice.

from azul import *


class InfinityState:
    def __init__(self):
        self.file_paths = [f"image_{i:04d}.png" for i in range(1000)]
        self.visible_start = 0
        self.visible_count = 50


def layout(data, info):
    title = (Dom.create_text(
        f"Infinite Gallery - {len(data.file_paths)} images")
        .with_css("font-size:20px;margin-bottom:10px;"))

    end = min(data.visible_start + data.visible_count, len(data.file_paths))
    container = Dom.create_div().with_css(
        "display:flex;flex-wrap:wrap;gap:10px;padding:10px;"
        "flex-grow:1;overflow:scroll;background:#f5f5f5;")

    for i in range(data.visible_start, end):
        item = (Dom.create_div()
                .with_css("width:150px;height:150px;background:white;"
                          "border:1px solid #ddd;display:flex;"
                          "align-items:center;justify-content:center;")
                .with_child(Dom.create_text(data.file_paths[i])))
        container = container.with_child(item)

    body = (Dom.create_body()
            .with_css("padding:20px;font-family:sans-serif;")
            .with_child(title)
            .with_child(container))
    return body.style(Css.empty())


state = InfinityState()
window = WindowCreateOptions.create(layout)
app = App.create(state, AppConfig.create())
app.run(window)
Background threads and timers for non-blocking operations Background threads and timers for non-blocking operations Background threads and timers for non-blocking operations

True Native
Multithreading

Real OS threads, not JavaScript promises. Background tasks with automatic UI updates. Spawn database queries, file operations, or network requests without freezing your app. Timers, thread pools, and progress callbacks built-in.

more languages…
# Async Operations - Python
# python async.py

from azul import *


class AsyncState:
    def __init__(self):
        # not_connected, connecting, loading, loaded, error
        self.stage = "not_connected"
        self.database_url = "postgres://localhost:5432/mydb"
        self.loaded_data = []
        self.progress = 0.0
        self.error_message = ""


CLICK = EventFilter.Hover(HoverEventFilter.MouseUp())


def connect_button(data):
    return (Dom.create_div()
            .with_css("padding:10px 20px;background:#4CAF50;color:white;cursor:pointer;")
            .with_child(Dom.create_text("Connect"))
            .with_callback(CLICK, data, start_connection))


def reset_button(data):
    return (Dom.create_div()
            .with_css("padding:10px;background:#2196F3;color:white;cursor:pointer;")
            .with_child(Dom.create_text("Reset"))
            .with_callback(CLICK, data, reset_connection))


def progress_view(data):
    return (Dom.create_div()
            .with_child(Dom.create_text(f"Progress: {int(data.progress)}%"))
            .with_child(ProgressBar.create(data.progress).dom()))


def loaded_view(data):
    return (Dom.create_div()
            .with_child(Dom.create_text(f"Loaded {len(data.loaded_data)} records"))
            .with_child(reset_button(data)))


def layout(data, info):
    title = (Dom.create_text("Async Database Connection")
             .with_css("font-size:24px;margin-bottom:20px;"))

    if data.stage == "not_connected":
        content = connect_button(data)
    elif data.stage in ("connecting", "loading"):
        content = progress_view(data)
    elif data.stage == "loaded":
        content = loaded_view(data)
    else:
        content = Dom.create_text(data.error_message)

    body = (Dom.create_body()
            .with_css("padding:30px;font-family:sans-serif;")
            .with_child(title)
            .with_child(content))

    return body.style(Css.empty())


def start_connection(data, info):
    data.stage = "connecting"
    data.progress = 0.0
    return Update.RefreshDom()


def reset_connection(data, info):
    data.stage = "not_connected"
    data.progress = 0.0
    data.loaded_data = []
    data.error_message = ""
    return Update.RefreshDom()


state = AsyncState()
window = WindowCreateOptions.create(layout)
app = App.create(state, AppConfig.create())
app.run(window)
XHTML site rendered with Azul XHTML site rendered with Azul XHTML site rendered with Azul

Built-in CSS
Styling Engine

Block, inline, flexbox, grid - the same layout power as modern browsers, but without any bloat. Load XHTML from files or strings. Hot-reload your UI without recompiling. Perfect for designers: tweak styles in CSS, see changes instantly.

more languages…
# XHTML file loading and rendering example
# Xml::from_str(s) returns a Result wrapper - match on Ok / Err.

from azul import *


def layout(data, info):
    src = open("assets/spreadsheet.xhtml").read()
    parsed = Xml.from_str(src)
    if parsed.is_ok():
        return Dom.create_from_parsed_xml(parsed.unwrap())
    return Dom.create_body()


app = App.create(None, AppConfig.create())
window = WindowCreateOptions.create(layout)
app.run(window)
Calculator built with CSS Grid layout Calculator built with CSS Grid layout Calculator built with CSS Grid layout

Flexbox and
Grid Layouts

Modern CSS Grid and Flexbox layouts, powered by the Taffy engine. Build responsive, complex interfaces without learning yet another layout system. Just the same CSS you know from the web - without the browser baggage.

more languages…
# Calculator with CSS Grid - Python
# Demonstrates CSS Grid layout and component composition

from azul import *


class Calculator:
    def __init__(self):
        self.display = "0"
        self.current_value = 0.0
        self.pending_operation = None
        self.pending_value = None
        self.clear_on_next_input = False

    def input_digit(self, digit):
        if self.clear_on_next_input:
            self.display = ""
            self.clear_on_next_input = False
        if self.display == "0" and digit != ".":
            self.display = digit
        elif digit == "." and "." in self.display:
            pass
        else:
            self.display += digit
        self.current_value = float(self.display) if self.display else 0.0

    def set_operation(self, op):
        self.calculate()
        self.pending_operation = op
        self.pending_value = self.current_value
        self.clear_on_next_input = True

    def calculate(self):
        if self.pending_operation is None or self.pending_value is None:
            return
        if self.pending_operation == "add":
            result = self.pending_value + self.current_value
        elif self.pending_operation == "subtract":
            result = self.pending_value - self.current_value
        elif self.pending_operation == "multiply":
            result = self.pending_value * self.current_value
        elif self.pending_operation == "divide":
            if self.current_value != 0:
                result = self.pending_value / self.current_value
            else:
                self.display = "Error"
                self.pending_operation = None
                self.pending_value = None
                return
        else:
            return
        self.current_value = result
        if result == int(result) and abs(result) < 1e15:
            self.display = str(int(result))
        else:
            self.display = str(result)
        self.pending_operation = None
        self.pending_value = None
        self.clear_on_next_input = True

    def clear(self):
        self.display = "0"
        self.current_value = 0.0
        self.pending_operation = None
        self.pending_value = None
        self.clear_on_next_input = False

    def invert_sign(self):
        self.current_value = -self.current_value
        self.display = (str(int(self.current_value))
                        if self.current_value == int(self.current_value)
                        else str(self.current_value))

    def percent(self):
        self.current_value /= 100.0
        self.display = str(self.current_value)


CALC_STYLE = ("height:100%;display:flex;flex-direction:column;"
              "font-family:sans-serif;")
DISPLAY_STYLE = ("background-color:#2d2d2d;color:white;font-size:48px;"
                 "text-align:right;padding:20px;display:flex;align-items:center;"
                 "justify-content:flex-end;min-height:80px;")
BUTTONS_STYLE = ("flex-grow:1;display:grid;"
                 "grid-template-columns:1fr 1fr 1fr 1fr;"
                 "grid-template-rows:1fr 1fr 1fr 1fr 1fr;gap:1px;"
                 "background-color:#666666;")
BTN_STYLE = ("background-color:#d1d1d6;color:#1d1d1f;font-size:24px;"
             "display:flex;align-items:center;justify-content:center;")
OP_STYLE = ("background-color:#ff9f0a;color:white;font-size:24px;"
            "display:flex;align-items:center;justify-content:center;")
ZERO_STYLE = ("background-color:#d1d1d6;color:#1d1d1f;font-size:24px;"
              "display:flex;align-items:center;justify-content:flex-start;"
              "padding-left:28px;grid-column:span 2;")


def make_callback(calc, event_type, event_data):
    def cb(data, info):
        if event_type == "digit":
            calc.input_digit(event_data)
        elif event_type == "operation":
            calc.set_operation(event_data)
        elif event_type == "equals":
            calc.calculate()
        elif event_type == "clear":
            calc.clear()
        elif event_type == "invert":
            calc.invert_sign()
        elif event_type == "percent":
            calc.percent()
        return Update.RefreshDom()
    return cb


def button(calc, label, event_type, event_data, style):
    return (Dom.create_div()
            .with_css(style)
            .with_child(Dom.create_text(label))
            .with_callback(
                EventFilter.Hover(HoverEventFilter.MouseUp()),
                calc,
                make_callback(calc, event_type, event_data)))


def layout(data, info):
    display = (Dom.create_div()
               .with_css(DISPLAY_STYLE)
               .with_child(Dom.create_text(data.display)))

    rows = [
        ("C", "clear", None, BTN_STYLE),
        ("+/-", "invert", None, BTN_STYLE),
        ("%", "percent", None, BTN_STYLE),
        ("÷", "operation", "divide", OP_STYLE),
        ("7", "digit", "7", BTN_STYLE),
        ("8", "digit", "8", BTN_STYLE),
        ("9", "digit", "9", BTN_STYLE),
        ("×", "operation", "multiply", OP_STYLE),
        ("4", "digit", "4", BTN_STYLE),
        ("5", "digit", "5", BTN_STYLE),
        ("6", "digit", "6", BTN_STYLE),
        ("-", "operation", "subtract", OP_STYLE),
        ("1", "digit", "1", BTN_STYLE),
        ("2", "digit", "2", BTN_STYLE),
        ("3", "digit", "3", BTN_STYLE),
        ("+", "operation", "add", OP_STYLE),
        ("0", "digit", "0", ZERO_STYLE),
        (".", "digit", ".", BTN_STYLE),
        ("=", "equals", None, OP_STYLE),
    ]

    buttons = Dom.create_div().with_css(BUTTONS_STYLE)
    for label, evt, evt_data, style in rows:
        buttons = buttons.with_child(button(data, label, evt, evt_data, style))

    body = (Dom.create_div()
            .with_css(CALC_STYLE)
            .with_child(display)
            .with_child(buttons))

    return body.style(Css.empty())


def main():
    calc = Calculator()
    app = App.create(calc, AppConfig.create())
    window = WindowCreateOptions.create(layout)
    app.run(window)


if __name__ == "__main__":
    main()