Skip to content
📖 10 min🎯Difficulty:
Prerequisites:Components vs Services vs Utilities

The Problem

When your shell starts with one shell.qml file and grows to dozens of components, a flat directory becomes unmanageable. Files are hard to find, imports are fragile, and there's no convention for where new files should go.

The Naive Approach

~/.config/quickshell/
  shell.qml
  bar.qml
  clock.qml
  cpu-widget.qml
  ram-widget.qml
  network-widget.qml
  weather-service.qml
  theme-utils.qml
  ...

Everything is in one folder. Imports use "foo.qml" as relative paths. There's no way to tell if something is a component, service, or utility without reading its contents.

💡Mental Model

Think of your project folder like a workshop. Components are the finished tools on the wall — ready to grab and use. Services are the workbench — they transform raw materials. Utilities are the storage bins — organized by type (nuts, bolts, screws). A messy workshop leads to lost time finding the right tool.

quickshell/
  shell.qml                      # Entry point
  config.qml                     # User configuration singleton
  theme.qml                      # Color palette and sizing

  bar/
    MainBar.qml                  # Main panel window
    LeftSection.qml              # Workspace indicators, launcher
    CenterSection.qml            # Clock, date
    RightSection.qml             # System tray, quick toggles

  popups/
    AppLauncher.qml              # Application launcher popup
    NotificationCenter.qml       # Notification list popup
    ControlCenter.qml            # Quick settings popup
    CalendarPopop.qml            # Calendar/date picker

  widgets/
    CpuWidget.qml                # CPU usage bar
    RamWidget.qml                # Memory usage bar
    DiskWidget.qml               # Disk usage bar
    NetworkWidget.qml            # Network speed indicator
    BatteryWidget.qml            # Battery percentage and icon
    ClockWidget.qml              # Digital clock display
    MprisWidget.qml              # Now-playing media widget

  services/
    WeatherService.qml           # Weather data singleton
    WallpaperService.qml         # Wallpaper rotation
    UpdateService.qml            # Package update checker

  util/
    format.js                    # Number/date formatting helpers
    color.js                     # Color manipulation utilities
    math.js                      # Math helper functions

  assets/
    icons/                       # Custom SVG icons
    fonts/                       # Bundled font files
    images/                      # Wallpapers, backgrounds

  tests/
    test-format.js               # Unit tests for utilities

Why This Works

Separation by concern. Each directory groups files by their architectural role (bar/, popups/, widgets/, services/, util/). A developer opening the project for the first time knows where to find each piece.

Implicit imports via folders. Quickshell supports qs. module imports (v0.2.0+):

qml
import qs.widgets
import qs.services
import qs.popups
import "util/format.js" as Format

This is cleaner than listing every individual file path.

No circular imports. Services live in services/ and never import from widgets/ or bar/. Components import from services. Utilities import nothing from the project.

Assets are discoverable. Icons, fonts, and images have dedicated directories instead of being scattered across the project.

File Naming Conventions

PatternExampleRule
PascalCase for typesCpuWidget.qmlEvery QML type starts with uppercase
camelCase for instancescpuWidgetid properties use camelCase
kebab-case for assetsbattery-icon.svgAssets use lowercase dash-separated
.js for utilitiesformat.jsPure JS files use lowercase

The qs Module Import

In Quickshell v0.2.0+, the qs import prefix lets you import an entire directory as a module:

qml
import qs.widgets

// Now use any QML file in widgets/ directly:
CpuWidget {}
RamWidget {}

This replaces explicit relative imports like import "../widgets/CpuWidget.qml" and makes refactoring easier — move a file within widgets/ and all import qs.widgets statements still work.

Exercises

Exercise
Convert a flat shell project to the recommended structure. Move each file to its appropriate subdirectory and update all import paths.
⭐⭐Exercise
Add a `config.qml` singleton to the project root that exposes user preferences (panel height, colors, enabled widgets). Import it as `import qs` and read values throughout your components.
⭐⭐⭐Exercise
Set up a `tests/` directory with automated QML tests. Write a test for `format.js` that verifies `bytesToHuman(1024)` returns `"1.0 KB"`. Integrate the test into a CI pipeline using `qmllint` and `qmltestrunner`.
What You've Learned
  • Group files by architectural role (bar, popups, widgets, services, util)
  • Use qs. module imports for clean, refactorable paths
  • Follow naming conventions: PascalCase for types, camelCase for ids
  • Keep assets in dedicated directories, not mixed with source code

Built with VitePress. Licensed under MIT.