Building a QML Bar and Panel from Scratch with Quickshell
Score —A guide to creating a lightweight, animated and modular top bar (panel) and status widget for Wayland using Quickshell components.
Contents
Summary: A step-by-step walkthrough of how to write an Omarchy-style top bar with Quickshell: a directory structure split into modules under
~/.config/quickshell/,shell.qmldefining the screen layer,Workspaces.qmlshowing Hyprland workspaces as clickable buttons, and smooth transitions withBehavior onblocks.
When you want to develop a personal top bar (panel) in a Wayland environment, you traditionally faced two options: GTK-based panels limited to static configuration files, or excessively heavy JavaScript solutions running a browser engine. Quickshell brings together the best of both worlds by connecting QML's powerful declarative structure directly to Wayland protocols.
In this article, we'll go through step by step how you can build a lightweight, modular top bar with fluid animations in the Omarchy style using Quickshell.
1. Directory Structure and Basic Configuration
Quickshell configurations are located by default in the ~/.config/quickshell/ directory. For a clean architecture, splitting components into modules is the healthiest approach:
~/.config/quickshell/
├── shell.qml # Ana giriş noktası
├── components/
│ ├── Workspaces.qml # Çalışma alanları modülü
│ ├── WindowTitle.qml # Aktif pencere başlığı
│ └── SystemTray.qml # Sistem tepsisi ve göstergeler
└── theme/
└── Theme.qml # Global renk ve tipografi tanımları
2. Main Shell: shell.qml
The shell.qml file defines the screen layer and the dimensions of the main frame:
import QtQuick 2.15
import QtQuick.Layouts 1.15
import Quickshell 1.0
import Quickshell.Wayland 1.0
import "components"
import "theme"
Scope {
PanelWindow {
id: bar
anchors {
top: true
left: true
right: true
}
height: 36
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusiveZone: 36
color: Theme.background
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 8
// Sol: Çalışma Alanları
Workspaces {
Layout.alignment: Qt.AlignLeft
}
// Orta: Aktif Pencere
WindowTitle {
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
}
// Sağ: Sistem Durumu
SystemTray {
Layout.alignment: Qt.AlignRight
}
}
}
}
3. Workspace Indicator: Workspaces.qml
Listing Hyprland workspaces as dynamic buttons and making them clickable with the mouse is quite simple:
import QtQuick 2.15
import QtQuick.Layouts 1.15
import Quickshell.Hyprland 1.0
import "../theme"
RowLayout {
spacing: 4
Repeater {
model: 5
Rectangle {
id: wsButton
property int wsId: index + 1
property bool isActive: Hyprland.activeWorkspace.id === wsId
width: 24
height: 24
radius: 4
color: isActive ? Theme.accent : Theme.surface
border.color: isActive ? Theme.accent : Theme.border
Text {
anchors.centerIn: parent
text: wsId
font.family: "JetBrains Mono"
font.pixelSize: 11
font.bold: isActive
color: isActive ? Theme.background : Theme.textMuted
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Hyprland.dispatch("workspace " + wsId)
}
Behavior on color {
ColorAnimation { duration: 120 }
}
}
}
}
4. Smooth Transitions and Hover Effects
The biggest advantage of QML is that it offers hardware-accelerated and physics-based interpolation beyond CSS animations. For example, a button glowing softly when the mouse hovers over it is solved in a single line with Behavior on ... blocks.
Since components developed with Quickshell are drawn on a single surface on the GPU rather than as separate windows, no frame drops occur.
My reading
- Splitting components into modules makes maintenance easier: each part lives in its own QML file.
- Animation is inside QML: transitions are written with
Behavior onwithout an additional library. - Drawing on a single surface provides smoothness: components are not separate windows but are drawn on the same GPU surface.