Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

window

An xdg_toplevel: an application window the compositor places, tiles, decorates and closes. Use it for a settings window or a dialog; use a panel for anything pinned to the desktop. Rules every role shares are in surfaces.

local open = state("settings_open", false)
local page = state("settings_page", "General")

local function tab(name)
    return button {
        width = "Fill",
        padding = { left = 12, right = 12, top = 8, bottom = 8 },
        radius = 8,
        background = page:map(function(current) return current == name and "#313244" or "#00000000" end),
        on_click = function() page:set(name) end,
        children = { text { content = name, foreground = "#cdd6f4" } },
    }
end

local settings = window {
    id = "settings",
    title = page:map(function(name) return "Settings: " .. name end),
    app_id = "org.example.settings",
    min_size = { width = 480, height = 360 },
    visible = open,
    on_close = function() open:set(false) end,
    child = row {
        width = "Fill",
        height = "Fill",
        background = "#1e1e2e",
        children = {
            column { width = 160, height = "Fill", padding = 8, spacing = 4, background = "#181825",
                children = { tab("General"), tab("Display"), tab("Sound"), tab("Power") } },
            column { width = "Fill", padding = 24,
                children = { text { content = page, font_size = 20, foreground = "#cdd6f4" } } },
        },
    },
}

return { settings }

mantle toggle settings_open opens it; the compositor’s close button or keybind closes it through on_close. The title follows the selected tab.

Properties

Beyond the shared properties. Every field but id and on_close takes a signal and updates the open window in place; a change while it is closed applies when it next opens.

PropertyTypeDefaultBehaviour
idstringRequiredThe surface’s identity across reloads, unique among surfaces. A panel’s or lock’s per-output instances are "{id}@{output}"; monitor = "Active" keeps the bare id
titlestring|Bound""The window title
app_idstring|Bound"mantle-{id}"What compositor window rules match
min_size{ width: number, height: number }|Bound, [0, 8192]NoneAdvisory hint to the compositor; layout does not enforce it. Both keys required, 0 leaves that axis unconstrained. Also the opening size on an axis the compositor leaves to the client (size)
max_size{ width: number, height: number }|Bound, [0, 8192]NoneAdvisory, as min_size. A non-zero axis below min_size’s is refused; also clamps the opening size
on_closefun()NoneThe user asked to close. The window stays open until the config sets visible = false; without a handler a close request does nothing
visibleboolean|BoundtrueOpens and closes the window; state and id survive
widthLength|Bound, [0, 8192]Fill the windowThe root’s size inside the window, not the window’s (size)
heightLength|Bound, [0, 8192]Fill the windowAs width
childNode|BoundNoneThe one root node; a function child is refused

The engine requests server-side decorations and draws none itself. A compositor that insists on client-side decorations gets an undecorated window, with a log line.

Size

The window’s size is the compositor’s configure. The root fills it on each axis where it has no width/height of its own; a set one sizes the root inside the window.

CompositorOpening size
Tiling (niri, a tiled Hyprland window)The tile the compositor sends
Floating, leaving an axis to the clientA non-zero min_size on that axis, else 640×480, clamped by a non-zero max_size

To set a floating window’s size or position, use compositor rules on app_id, or min_size for the opening size.

How do I…

TaskAnswer
Open a settings window from a keybindBind visible to named state, as in the example; mantle toggle settings_open
Close it when the user clicks the close buttonon_close = function() open:set(false) end
Ask before closingConfirm before closing
Make it float, or place itA Hyprland windowrule or niri window-rule matching app_id
Give it a starting sizemin_size, or a compositor rule
Scroll content taller than the windowA column { height = "Fill", scroll = scroll("name") } (scroll)
Close it from a button inside itSet its visible state to false from on_click
Open a menu from itA popup with parent set to the window’s id

Confirm before closing

on_close is a request, so it can open a question instead of closing:

local open = state("editor_open", true)
local confirming = state("editor_confirm", false)

local editor = window {
    id = "editor",
    title = "Editor",
    visible = open,
    on_close = function() confirming:set(true) end,
    child = column {
        width = "Fill", height = "Fill", padding = 16, spacing = 8, background = "#1e1e2e",
        children = {
            text { content = "Unsaved changes", foreground = "#cdd6f4" },
            row {
                spacing = 8,
                visible = confirming,
                children = {
                    button { padding = 8, background = "#f38ba8",
                        on_click = function() confirming:set(false); open:set(false) end,
                        children = { text { content = "Discard" } } },
                    button { padding = 8, background = "#313244",
                        on_click = function() confirming:set(false) end,
                        children = { text { content = "Cancel", foreground = "#cdd6f4" } } },
                },
            },
        },
    },
}

return { editor }

Gotchas

TrapFix
The close button does nothingAdd on_close and set visible to false in it
min_size doesn’t stop the root shrinkingIt is advisory to the compositor; layout does not enforce it
min_size = { width = 400 } is refusedName both axes; 0 leaves one unconstrained
max_size below min_size is refusedKeep every non-zero max_size axis at or above min_size’s, or 0
width = 600 on the window doesn’t resize itThat sizes the root inside the window; the compositor owns the window’s size
A click on the window’s empty background reaches the window behind itPut the background on a "Fill" child, not the window (input region)
No title bar under a compositor without server-side decorationsThe engine draws none; draw your own row, or use compositor rules

See also: surfaces, popup, nodes, signals.

Source: window spec, window, root size.