Usage

Layouts

Vertical:

div().v_flex().gap(10).child_builder(text("A")).child_builder(text("B"))

Horizontal:

div().h_flex().gap(10).child_builder(button("Save")).child_builder(button("Cancel"))

Centered:

div().size_full().v_flex().items_center().justify_center()

Space between:

div().h_flex().justify_between()

Grid Layout

Basic 3-column grid:

div().grid_cols("1fr 1fr 1fr").gap(16)

Responsive-style grid with auto-fill:

div().grid_cols("repeat(auto-fill, minmax(200px, 1fr))").gap(16)

Item spanning multiple columns:

div().col("span 2")  # Spans 2 columns

2x2 grid with centered items:

(
    div()
    .grid_cols("1fr 1fr")
    .grid_rows("1fr 1fr")
    .gap(20)
    .place_center()
)

Styling

div()
.bg("#2d3748")
.text_color("#e2e8f0")
.padding(20)
.rounded(12)
.border(2, "#3182ce")

Example: per-side margins, borders and per-corner radii

div()
.mt(8)
.mr(12)
.mb(8)
.ml(12)
.border_top(2, "#e53e3e")
.border_bottom(2, "#38a169")
.rounded_tl(16)
.rounded_br(8)

Hover and Transitions

Buttons with hover:

button("Click me")
.bg("#fafafa")
.hover_bg("#d4d4d4")
.text_color("#0a0a0a")
.rounded(8)
.transition_all(0.2)

Input with focus:

input()
.bg("#171717")
.border(1, "#404040")
.focus_border_color("#a3a3a3")
.transition_colors(0.15)

Image with hover scale:

image("photo.jpg")
.width(120)
.height(120)
.object_fit("cover")
.cursor("pointer")
.transition_transform(0.3)
.hover_scale(1.05)

Partial Updates

set_root() uses DOM patching to update elements in place rather than replacing them. This preserves CSS transition state, so transitions animate smoothly across re-renders:

from wry_py import UiWindow, div, text, button

count = 0
window = UiWindow(title="Counter", width=400, height=300)

def get_color():
    if count > 0: return "#4ade80"
    if count < 0: return "#f87171"
    return "#ffffff"

def make_counter():
    return (
        text(f"Count: {count}")
        .id("counter")
        .text_size(32)
        .text_color(get_color())
        .transition_colors(0.3)
        .build()
    )

def increment():
    global count
    count += 1
    window.update_element("counter", make_counter())

root = (
    div()
    .size_full()
    .v_flex()
    .items_center()
    .justify_center()
    .gap(20)
    .child(make_counter())
    .child_builder(button("+").on_click(increment))
    .build()
)

window.set_root(root)
window.run()

For targeted updates, use update_element() with an element ID.

Local images

For environments where file:// access is restricted by the webview, use AssetCatalog to register asset bytes and reference them by name. The renderer will prefer the registered asset (embedded as a data URI) which avoids local-file permission issues.

Example (register and use an asset):

from wry_py import AssetCatalog, image

catalog = AssetCatalog()
with open("examples/local_image/assets/logo.png", "rb") as f:
    catalog.add("logo.png", f.read())

image("asset:logo.png").width(120).height(120).object_fit("cover")

Images

from wry_py import image

image("https://example.com/photo.jpg")
.width(200)
.height(150)
.object_fit("cover")
.alt("Description")
.rounded(8)

Text Input

from wry_py import input

current_value = ""

def on_input(value: str):
    global current_value
    current_value = value

field = (
    input()
    .placeholder("Enter text...")
    .padding(8, 12)
    .border(1, "#ccc")
    .on_input(on_input)
)

Checkbox

from wry_py import checkbox

def on_change(checked: str):
    # checked is "true" or "false" as string
    print(f"Checked: {checked == 'true'}")

checkbox("Accept terms").checked(False).on_change(on_change)

Radio Buttons

Radio buttons with the same group() are mutually exclusive:

from wry_py import div, radio

selected = "email"

def on_change(value: str):
    global selected
    selected = value

(
    div().v_flex().gap(8)
    .child_builder(radio("Email").group("contact").value("email").checked(True).on_change(on_change))
    .child_builder(radio("Phone").group("contact").value("phone").on_change(on_change))
    .child_builder(radio("SMS").group("contact").value("sms").on_change(on_change))
)

Select Dropdown

from wry_py import select

def on_change(value: str):
    print(f"Selected: {value}")

(
    select()
    .option("", "Choose...")
    .option("us", "United States")
    .option("uk", "United Kingdom")
    .option("ca", "Canada")
    .selected("us")
    .on_change(on_change)
)

Form Example

from wry_py import UiWindow, div, text, input, button

name = ""

def on_name_input(value):
    global name
    name = value

def on_submit():
    print(f"Submitted: {name}")

root = (
    div()
    .v_flex()
    .gap(12)
    .padding(20)
    .child_builder(text("Name:"))
    .child_builder(
        input()
        .placeholder("Enter name")
        .padding(8, 12)
        .border(1, "#ccc")
        .on_input(on_name_input)
    )
    .child_builder(
        button("Submit")
        .padding(10, 20)
        .bg("#48bb78")
        .text_color("#fff")
        .on_click(on_submit)
    )
    .build()
)

Todo List Example

from wry_py import AppBase, UiWindow, div, text, button

class TodoApp(AppBase):
    def __init__(self):
        super().__init__()
        self.items: list[str] = []

    def add_item(self):
        self.items.append(f"Item {len(self.items) + 1}")
        self.render()

    def remove_item(self, index: int):
        if 0 <= index < len(self.items):
            del self.items[index]
            self.render()

    def render(self):
        item_list = div().v_flex().gap(4)
        if not self.items:
            item_list = item_list.child_builder(
                text("No items yet").text_color("#94a3b8").text_size(16)
            )
        for i, item in enumerate(self.items):
            item_list = item_list.child_builder(
                div()
                .h_flex()
                .justify_between()
                .items_center()
                .padding(8)
                .bg("#f8fafc")
                .child_builder(text(item).text_color("#1e293b"))
                .child_builder(
                    button("Remove")
                    .padding(6, 12)
                    .bg("#ef4444")
                    .text_color("#fff")
                    .rounded(4)
                    .on_click(lambda idx=i: self.remove_item(idx))
                )
            )

        root = (
            div()
            .size_full()
            .v_flex()
            .padding(20)
            .gap(16)
            .child_builder(text("Todo List").text_size(24).text_weight("bold"))
            .child_builder(
                button("Add Item")
                .padding(10, 16)
                .bg("#4299e1")
                .text_color("#fff")
                .on_click(self.add_item)
            )
            .child_builder(item_list)
            .build()
        )
        if self.window:
            self.window.set_root(root)

window = UiWindow(title="Todo", width=400, height=500)
app = TodoApp()
app.set_window(window)
app.run()