Building Your Desktop Shell with Quickshell
Quickshell is a toolkit for building your own desktop shell — bars, widgets, launchers, notification popups — entirely in QML. No forking someone else's bar, no C++ plugin dance. You write declarative QtQuick and it renders as native Wayland layer-shell surfaces.
Why Quickshell
Most status bars (Waybar, Polybar) give you config knobs but stop where their
authors stopped. Want a custom media widget that reacts to playerctl? A bar
that reshapes per-monitor? You hit the wall. Quickshell hands you the whole
canvas instead.
Quickshell is a framework, not a ready-made bar. You build the shell. The payoff is total control; the cost is you write QML.
Quickshell targets Wayland compositors with wlr-layer-shell (Hyprland, Niri,
Sway, river). X11 is not supported.
Installation
# Arch (AUR)
yay -S quickshell
# Nix
nix run github:quickshell-mirror/quickshell
Your first bar
A shell is just a QML entry point. ShellRoot is the top; PanelWindow is a
layer-shell surface anchored to a screen edge.
import Quickshell
import QtQuick
ShellRoot {
PanelWindow {
anchors { top: true; left: true; right: true }
implicitHeight: 32
Text {
anchors.centerIn: parent
text: "hello from quickshell"
color: "white"
}
}
}
Run it:
quickshell -p shell.qml
Live data with a Process
Bars are boring without live data. Process runs a command and streams its
output back into reactive properties.
import Quickshell
import Quickshell.Io
import QtQuick
Text {
id: clock
color: "white"
Process {
id: date
command: ["date", "+%H:%M"]
stdout: SplitParser {
onRead: (line) => clock.text = line
}
}
Timer {
interval: 1000; running: true; repeat: true
onTriggered: date.running = true
}
}
SplitParser splits stdout on newlines and fires onRead per line — no manual
buffering. For long-lived streams (like playerctl --follow), just leave the
process running instead of polling with a Timer.
From polling to events
Polling date every second works, but event-driven sources scale better:
- Timer { interval: 1000; running: true; onTriggered: proc.running = true }
- Process { command: ["playerctl", "metadata", "--format", "{{title}}"] }
+ Process {
+ command: ["playerctl", "--follow", "metadata", "--format", "{{title}}"]
+ running: true
+ stdout: SplitParser { onRead: (t) => label.text = t }
+ }
Bar frameworks compared
| Tool | Language | Custom widgets | Per-monitor | Layer-shell |
|---|---|---|---|---|
| Waybar | JSON/CSS | limited | ✅ | ✅ |
| Polybar | INI | scripts only | ✅ | ❌ (X11) |
| Quickshell | QML | unlimited | ✅ | ✅ |
Checklist before you ship your shell
- Install quickshell
- Render a
PanelWindow - Wire live data via
Process - Add a notification daemon (
Quickshell.Services.Notifications) - Split components into reusable QML files
A shell you built yourself is a shell you can actually fix at 3am. 1
Where to go next
The stdlib modules — Quickshell.Services.Mpris, .SystemTray,
.Notifications, .Pipewire — cover most of what a daily-driver bar needs.
Wire them into components and you have a shell nobody else has.
Footnotes
-
Which is the whole point — no upstream to wait on. ↩