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

popup

An xdg_popup on a shown panel, window or popup: a dropdown, context menu or tooltip. The compositor places it against a rectangle in the parent, keeps it on screen and, with a grab, dismisses it on an outside click, which a second panel cannot do. A hidden popup has no Wayland object. Rules every role shares are in surfaces.

local menu_open = state("menu_open", false)
local menu_anchor = state("menu_anchor", { x = 0, y = 0, width = 1, height = 1 })

local bar = panel {
    id = "bar",
    layer = "Top",
    anchor = { top = true, left = true, right = true },
    width = "Fill",
    height = 32,
    exclusive = true,
    child = row {
        width = "Fill", height = "Fill", background = "#1e1e2e",
        children = {
            button {
                padding = 8,
                on_click = function(rect)
                    menu_anchor:set(rect)
                    menu_open:set(not menu_open:get())
                end,
                children = { text { content = "Menu", foreground = "#cdd6f4" } },
            },
        },
    },
}

local menu = popup {
    id = "menu",
    parent = "bar",
    anchor_rect = menu_anchor,
    anchor = "Bottom",
    gravity = "Bottom",
    offset = { y = 4 },
    visible = menu_open,
    on_dismiss = function() menu_open:set(false) end,
    child = column {
        width = 160, padding = 12, spacing = 10, radius = 10, background = "#1e1e2e",
        border_width = 1, border_color = "#45475a",
        children = {
            text { content = "Settings", foreground = "#cdd6f4" },
            text { content = "Log out", foreground = "#cdd6f4" },
        },
    },
}

return { bar, menu }

A dropdown under a bar button. on_click’s rect is the button in its surface’s coordinates, which is what anchor_rect takes (pointer input). An outside click dismisses it and on_dismiss clears the state.

Properties

Beyond the shared properties. Every field but id, parent and on_dismiss takes a signal.

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
parentstringRequiredThe id of a shown panel, window or popup; hiding the parent closes this popup. On a per-output panel it opens on the clicked instance, else the first. A change applies at the next open; a lock cannot be a parent
anchor_rectRect|BoundRequiredIn the parent’s surface coordinates; width/height in (0, 8192], x/y default 0. Usually the rect on_click passes
anchor"Top"|"Bottom"|"Left"|"Right"|"TopLeft"|"TopRight"|"BottomLeft"|"BottomRight"|"Center"|Bound"Center"The point on anchor_rect the popup hangs from
gravity"Top"|"Bottom"|"Left"|"Right"|"TopLeft"|"TopRight"|"BottomLeft"|"BottomRight"|"Center"|Bound"Center"The direction it extends from that point: "Bottom" hangs it below, "BottomRight" below and to the right
constraint_adjustment("SlideX"|"SlideY"|"FlipX"|"FlipY"|"ResizeX"|"ResizeY")[]|Bound{ "FlipY", "SlideX" }How the compositor may keep it on screen; {} for none, order is ignored
offset{ x?: number, y?: number }|Bound{ x = 0, y = 0 }Pixel nudge after anchor and gravity; an absent axis is 0, negative moves up or left
widthnumber|BoundContentPixels in (0, 8192]; no "Fill" or %. Omitted sizes to the content, capped at the first output’s size and the root’s max_width/max_height; an open popup follows it through xdg_popup.reposition (xdg-shell v3+)
heightnumber|BoundContentAs width; each axis is independent
grabboolean|BoundtrueTakes an input grab so an outside click dismisses it (grab). false for a tooltip
on_dismissfun()NoneThe compositor closed it (click outside, denied grab, parent gone); not called when the config hides it. Set visible = false here, or it reopens on the next click
visibleboolean|BoundtrueOpens and closes the popup; state and id survive
childNode|BoundNoneThe one root node; a function child is refused

Placement

While a popup is open, a change to its measured size or to any positioner field moves it through xdg_popup.reposition. Below xdg_popup version 3 it keeps the size and place it opened at until it closes, and logs a warning.

Grab

A grab needs a left, right or middle press or release on one of the shell’s surfaces in the same turn, so open a grabbing popup from on_click. Without one the popup stays closed and a warning is logged. A compositor that denies the grab dismisses the popup, and on_dismiss runs.

Dismissal

A compositor dismissal destroys the popup but leaves your visible signal true. The engine latches it shut until the next pointer press or release on the shell, then opens it again. Clear your state in on_dismiss, as the example at the top does.

Hiding or dismissing a popup also closes the popups open under it and latches them the same way, so each one whose visible stays true reopens on the next press.

How do I…

TaskAnswer
Show a dropdown under a bar buttonThe example at the top
Open a submenu from a menuNested menus
Show a tooltip on hoverTooltip
Anchor to a node without clicking or hovering itAnchor to a node’s geometry
Open a context menu on right clickon_click = function(rect, which) if which == "right" then ... end end (pointer)
Fade it out before it closesKeep visible true with delay while the child’s opacity animates (delay)
Open it from a keybindgrab = false, since a keybind is no pointer press; it then stays until the config hides it
Keep it on screen near an edgeconstraint_adjustment = { "FlipX", "FlipY", "SlideX", "SlideY" }
Give it a fixed sizewidth and height in px
Open it from a windowparent = "<window id>"

Nested menus

A popup can parent another popup. The submenu hangs off the right edge of the clicked row and flips left near the screen edge:

local sub_open = state("sub_open", false)
local sub_anchor = state("sub_anchor", { x = 0, y = 0, width = 1, height = 1 })

local power_row = button {
    padding = 4,
    on_click = function(rect)
        sub_anchor:set(rect)
        sub_open:set(true)
    end,
    children = { text { content = "Power ›" } },
}

local submenu = popup {
    id = "power_menu",
    parent = "menu",
    anchor_rect = sub_anchor,
    anchor = "TopRight",
    gravity = "BottomRight",
    constraint_adjustment = { "FlipX", "SlideY" },
    visible = sub_open,
    on_dismiss = function() sub_open:set(false) end,
    background = "#1e1e2e", padding = 8,
    child = column { spacing = 4, children = { text { content = "Suspend" }, text { content = "Reboot" } } },
}

power_row goes inside the menu popup. on_click’s rect is in the menu’s own coordinates, which is what a child popup’s anchor_rect expects. Set sub_open to false wherever the menu closes (its on_dismiss included), or the submenu reopens on the next click.

Tooltip

A hover opens no grab, so grab = false. hover_rect tracks the hovered node and is 1×1 before the first hover, which keeps anchor_rect valid:

local clock = text { content = "12:30", padding = 8, hover = hover("clock") }

local tooltip = popup {
    id = "clock_tooltip",
    parent = "bar",
    anchor_rect = hover_rect("clock"),
    anchor = "Bottom",
    gravity = "Bottom",
    offset = { y = 4 },
    grab = false,
    visible = hover("clock"),
    background = "#1e1e2e", radius = 6, padding = 6,
    child = text { content = "Thursday, 24 September" },
}

clock goes inside the bar panel.

Anchor to a node’s geometry

Bind geometry. It reads zero before the first layout, so map it to a 1×1 fallback. With no click to open it, it needs grab = false:

local battery = text { content = "87%", padding = 8, geometry = geometry("battery") }

local details = popup {
    id = "battery_details",
    parent = "bar",
    anchor_rect = geometry("battery"):map(function(rect)
        return rect.width > 0 and rect or { x = 0, y = 0, width = 1, height = 1 }
    end),
    anchor = "Bottom",
    gravity = "Bottom",
    grab = false,
    visible = state("battery_open", false),
    child = text { content = "2 h 10 min left" },
}

Gotchas

TrapFix
anchor_rect with a zero width or height is refused (a geometry before first layout, a hand-built rect)Fall back to { x = 0, y = 0, width = 1, height = 1 }
A dropdown reopens on the next click after an outside click closed itSet its visible state to false in on_dismiss
A popup that is visible at startup, or opened from a keybind, never opensgrab = true needs a click; open it from on_click, or set grab = false
A popup whose parent is hidden does not openShow the parent first; the popup opens on the next pass
A submenu reopens after its menu closedClear the submenu’s state wherever the menu closes, on_dismiss included
width = "Fill" or "50%" is refusedpx, or omit it to size to the content
anchor_rect from a click in a popup is placed wrong on the barA rect is in its own surface’s coordinates; anchor a popup only to rects from its parent
An open tooltip does not grow with its text on an old compositorxdg_popup below version 3 cannot reposition; it keeps its opening size until it closes

See also: surfaces, panel, input, signals.

Source: popup spec, popup, instances.