How to Define a Custom Bar Widget in Omarchy

Omarchy’s bar architecture relies on a widget registry pattern where each component extends the BarWidget base class and registers with the singleton BarWidgetRegistry to become visible in the UI.

Creating a custom bar widget in Omarchy requires extending the framework’s QML base classes and hooking into the centralized registry system. The basecamp/omarchy repository provides a standardized plugin architecture that manages widget lifecycle, settings injection, and IPC routing through specific base classes and registration APIs. This guide demonstrates the exact implementation patterns found in the source code.

Understanding the BarWidget Architecture

The Base Class (BarWidget.qml)

Every custom bar widget must extend BarWidget defined in shell/Ui/BarWidget.qml. This base class provides three critical injection properties that the bar supplies at runtime:

  • bar — The host Bar instance containing geometry, sizing, and positioning data.
  • moduleName — A unique string identifier used by the registry and the settings system.
  • settings — A per-widget configuration object read from shell.json.

The base class also exposes helper functions like broadcast() and setting() for inter-widget communication and configuration access.

The Widget Registry (BarWidgetRegistry.qml)

The BarWidgetRegistry in shell/services/BarWidgetRegistry.qml maintains the global catalogue of available widgets. It stores a map of { widgetId → { component, metadata } } and emits a changed() signal whenever the catalogue updates. The registry is instantiated once by shell.qml as a singleton property barWidgetRegistry, making it accessible to all plugin components.

Step-by-Step Implementation Guide

Extend BarWidget and Define moduleName

Create a QML file in a plugin directory (e.g., shell/plugins/bar/widgets/MyWidget.qml). Extend BarWidget and assign a unique moduleName that matches the ID you will use in shell.json.

// shell/plugins/bar/widgets/MyWidget.qml
import QtQuick
import qs.Commons
import qs.Ui

BarWidget {
    id: root
    moduleName: "omarchy.mywidget"
    
    // UI implementation here
}

Implement Panel Methods (Optional)

For widgets with expandable panels, implement the standard API so the bar can route IPC calls like shell.summon and shell.toggle:

  • open() — Display the panel.
  • close() — Hide the panel.
  • togglePanel() — Switch visibility state.
  • opened property — Boolean reflecting current state.

These methods allow the bar’s findPanelWidget logic to control your widget remotely.

Register with BarWidgetRegistry

In your plugin’s entry-point QML (typically Plugin.qml), call barWidgetRegistry.register() inside Component.onCompleted:

Component.onCompleted: {
    barWidgetRegistry.register(root.moduleName, root, { 
        title: "My Widget",
        icon: "my-icon"
    })
}

The registry stores this mapping and notifies the bar to instantiate your widget in the UI.

Configure in shell.json

Add a layout entry to ~/.config/omarchy/shell.json (or the default shell.json) to define placement and settings:

{
    "layout": [
        {
            "id": "omarchy.mywidget",
            "slot": "status",
            "settings": { "refreshInterval": 30 }
        }
    ]
}

The id must match your widget’s moduleName, and slot determines the bar position (e.g., "left", "center", "status", "right").

Complete Custom Bar Widget Example

Below is a minimal yet complete implementation following the patterns in shell/plugins/panels/clock/BarWidget.qml.

Widget component:

// File: shell/plugins/bar/widgets/MyWidget.qml
import QtQuick
import qs.Commons
import qs.Ui

BarWidget {
    id: root
    moduleName: "omarchy.mywidget"

    WidgetButton {
        id: button
        anchors.fill: parent
        bar: root.bar
        text: "MyWidget"
        tooltipText: "Click to toggle panel"
        onPressed: root.togglePanel()
    }

    Loader {
        id: panelLoader
        active: true
        source: Qt.resolvedUrl("Panel.qml")
        visible: false
        onLoaded: {
            if (panelLoader.item) {
                panelLoader.item.bar = root.bar
                panelLoader.item.settings = root.settings
                panelLoader.item.hostWidget = root
                panelLoader.item.anchorItem = button
            }
        }
    }

    readonly property bool opened: panelLoader.item ? panelLoader.item.opened : false

    function open()   { if (panelLoader.item) panelLoader.item.open() }
    function close()  { if (panelLoader.item) panelLoader.item.close() }
    function togglePanel() { if (panelLoader.item) panelLoader.item.toggle() }

    Component.onCompleted: {
        barWidgetRegistry.register(root.moduleName, root, { title: "My Widget" })
    }
}

Panel component:

// File: shell/plugins/bar/widgets/Panel.qml
import QtQuick

Item {
    id: panel
    property bool opened: false

    Rectangle {
        anchors.fill: parent
        color: "#202020"
        radius: 8
        Text {
            anchors.centerIn: parent
            color: "white"
            text: "Hello from MyWidget!"
        }
    }

    function open()   { opened = true; visible = true }
    function close()  { opened = false; visible = false }
    function toggle() { opened ? close() : open() }
}

Key Source Files and Reference Implementations

  • shell/Ui/BarWidget.qml — Base class defining injection properties (bar, moduleName, settings) and helper methods.
  • shell/services/BarWidgetRegistry.qml — Implements register(), unregister(), availableIds(), and metadataFor() APIs.
  • shell/plugins/panels/clock/BarWidget.qml — Production example showing IPC handling, panel management, and settings integration.
  • shell/plugins/panels/weather/BarWidget.qml — Alternative implementation demonstrating different UI patterns for the same architecture.
  • shell/shell.qml — Instantiates BarWidgetRegistry and wires it into the bar’s rendering logic.

Summary

  • Extend BarWidget to receive the three injection properties required for bar integration.
  • Call barWidgetRegistry.register() in your plugin entry-point to add the widget to the global catalogue.
  • Implement open(), close(), and togglePanel() if your widget needs an expandable panel controlled by IPC.
  • Match moduleName in code with the id field in shell.json for configuration and layout placement.
  • Reference shell/plugins/panels/clock/BarWidget.qml for production-ready patterns including panel injection and settings handling.

Frequently Asked Questions

What is the minimum code needed for a custom bar widget?

At minimum, create a QML file extending BarWidget with a unique moduleName property and call barWidgetRegistry.register(root.moduleName, root, {}) inside Component.onCompleted. You can omit panel functionality if you only need a static indicator button.

Where should I place my custom widget files?

Place QML files in a plugin subdirectory such as shell/plugins/bar/widgets/ or any path loaded by the plugin system. The entry-point QML file (often named Plugin.qml) must execute the registration code when loaded by shell.qml.

How does the bar know where to position my widget?

Positioning is controlled by the shell.json layout array. Each entry requires an id matching your widget’s moduleName and a slot field defining the bar region (e.g., "left", "center", "status", "right"). The bar iterates over barWidgetRegistry.availableIds() and places instances according to this configuration.

Can I update widget settings without restarting Omarchy?

Yes. The settings property injected by BarWidget reflects the configuration from shell.json. When you modify the JSON file, the settings object updates automatically, allowing your widget to react to configuration changes through property bindings without requiring a restart.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →