Machine translation from Turkish (Cloudflare Workers AI), not reviewed by the author. Read the Turkish original

← Posts

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.

· 2 min read
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.qml defining the screen layer, Workspaces.qml showing Hyprland workspaces as clickable buttons, and smooth transitions with Behavior on blocks.

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 on without an additional library.
  • Drawing on a single surface provides smoothness: components are not separate windows but are drawn on the same GPU surface.