The Problem
A desktop panel (a bar at the top or bottom of the screen) is not a regular application window. It must:
- Attach itself to the edge of the screen with no chrome or title bar.
- Reserve space so maximized windows don't overlap it.
- Stay above normal windows without stealing focus.
- Span the full width or height of the monitor.
Regular Window types from Qt Quick don't support these behaviors. They are managed by the window manager as ordinary toplevels.
The Naive Approach
You could try using a Window with flags: Qt.FramelessWindowHint and manually position it at x: 0, y: 0 with width: Screen.width. But this approach breaks immediately:
- The window manager still treats it as a floating window — maximized apps will cover it.
- There's no way to tell the compositor "reserve this space at the top."
- On Wayland, clients cannot set their own position. The compositor decides placement.
// This does NOT work reliably on Wayland:
Window {
x: 0; y: 0
width: Screen.width; height: 40
flags: Qt.FramelessWindowHint
// Maximized windows will cover this
}Think of the screen as a whiteboard. Regular windows are sticky notes — they can be moved around but stay on top of the board. A panel is a strip of magnetic tape attached to the edge of the whiteboard frame. It's always there, it doesn't cover the writing area, and it tells other sticky notes "this part of the board is taken."
The Idea
Quickshell provides PanelWindow — a window type built on the Wayland Layer Shell protocol. It understands the concept of screen edges, exclusive space, and layer ordering. A PanelWindow tells the compositor: "I am a panel, attach me to this edge, reserve this much space, and stay above normal windows."
import Quickshell
PanelWindow {
anchors.top: true
anchors.left: true
anchors.right: true
height: 36
exclusiveZone: 36
}This single declaration creates a bar that spans the top edge of the screen, reserves 36 pixels of space, and will not be covered by maximized windows.
Let's Build It
Let's build a minimal top bar with a clock:
import Quickshell
import QtQuick
import QtQuick.Layouts
PanelWindow {
id: topBar
height: 36
anchors {
top: true
left: true
right: true
}
exclusiveZone: height
aboveWindows: true
exclusionMode: ExclusionMode.Auto
Rectangle {
anchors.fill: parent
color: "#1a1b26"
RowLayout {
anchors {
fill: parent
leftMargin: 12
rightMargin: 12
}
spacing: 8
Text {
text: ""
font.pixelSize: 16
color: "#c0caf5"
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
Text {
id: clock
text: new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
font.pixelSize: 14
color: "#a9b1d6"
Layout.alignment: Qt.AlignVCenter
Timer {
interval: 1000
running: true
repeat: true
onTriggered: clock.text = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
}
}
}
}
}Let's Improve It
A static bar is a good start. Let's add a system tray area placeholder, make the background semi-transparent, and react to screen geometry changes:
PanelWindow {
id: bar
height: 36
anchors {
top: true
left: true
right: true
}
exclusiveZone: height
exclusionMode: ExclusionMode.Normal
Rectangle {
anchors.fill: parent
color: "#cc1a1b26" // semi-transparent
RowLayout {
anchors {
fill: parent
leftMargin: 12
rightMargin: 12
}
Text {
text: ""
font.pixelSize: 18
color: "#7aa2f7"
}
Text {
text: "Quickshell"
font.pixelSize: 12
color: "#565f89"
Layout.leftMargin: 4
}
Item { Layout.fillWidth: true }
Row {
spacing: 8
Layout.alignment: Qt.AlignVCenter
Text { text: ""; color: "#9ece6a"; font.pixelSize: 14 }
Text { text: ""; color: "#e0af68"; font.pixelSize: 14 }
Text { text: ""; color: "#7dcfff"; font.pixelSize: 14 }
}
Text {
id: clock
text: new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
font.pixelSize: 14
font.bold: true
color: "#c0caf5"
Layout.leftMargin: 12
Timer {
interval: 1000
running: true
repeat: true
onTriggered: clock.text = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
}
}
}
}
}Setting anchors to true vs anchoring to an item. On PanelWindow, the anchors properties (anchors.top, anchors.left, etc.) are booleans that tell the compositor which screen edge to attach to. This is different from QML's Item.anchors where you anchor to another item's edge. Mixing them up is a common source of confusion.
Forgetting exclusiveZone. Without an exclusiveZone, the compositor won't reserve space. Maximized windows will slide underneath your panel. Always set exclusiveZone to at least your panel's height (for horizontal bars) or width (for vertical bars).
Setting focusable: true. Panels should rarely be focusable. The default (false) is correct. Setting it to true means clicking the panel could steal focus from the active application.
Under the Hood
PanelWindow is Quickshell's QML wrapper around the Wayland Layer Shell protocol (zwlr_layer_shell_v1). When you create a PanelWindow:
- Quickshell creates a
wl_surfaceand attaches it to the layer shell. - The
anchorsbooleans are mapped to thezwlr_layer_surface_v1.anchorbitfield (top, bottom, left, right). - The
exclusiveZoneis sent viazwlr_layer_surface_v1.set_exclusive_zone. - The compositor responds by allocating the requested space and positioning the surface.
Because this all happens at the Wayland protocol level, the behavior is consistent across all compositors that implement the Layer Shell protocol (Hyprland, Sway, River, etc.).
PanelWindow internally derives from QsWindow, which manages the wl_surface lifecycle. The layer shell requests are sent during QQuickView::componentComplete(). If you change exclusiveZone or anchors at runtime, Quickshell sends the updated state to the compositor, which may re-layout its surfaces.
Use exclusionMode: ExclusionMode.Ignore when you want a floating overlay panel (e.g., a notification popup) that should not reserve space. Use ExclusionMode.Auto for normal panels that should push other windows away.
Diagram
graph TD
A[PanelWindow Declaration] --> B[Quickshell creates wl_surface]
B --> C[Attaches to zwlr_layer_shell_v1]
C --> D[Sends anchor flags]
C --> E[Sets exclusive zone]
C --> F[Sets layer: top]
D --> G[Compositor positions surface]
E --> G
F --> G
G --> H[Surface mapped to screen edge]
H --> I[Other windows respect exclusive zone]Exercises
- PanelWindow attaches to screen edges using boolean anchors
- exclusiveZone reserves screen space so maximized windows avoid the panel
- aboveWindows controls whether the panel renders above or below normal windows
- PanelWindow is backed by the Wayland Layer Shell protocol
- Panels should rarely be focusable