Skip to content
linux

Building Your Desktop Shell with Quickshell

Building Your Desktop Shell with Quickshell
3 min read

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

a minimal quickshell bar anchored to the top edge

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.

Read the official docs

Footnotes

  1. Which is the whole point — no upstream to wait on.

July 23, 2026

Comments