# How to Define a Custom Bar Widget in Omarchy

> Learn how to define a custom bar widget in Omarchy. Extend the BarWidget base class and register with BarWidgetRegistry to make your component visible in the UI.

- Repository: [37signals/omarchy](https://github.com/basecamp/omarchy)
- Tags: how-to-guide
- Published: 2026-08-23

---

**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`](https://github.com/basecamp/omarchy/blob/main/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`](https://github.com/basecamp/omarchy/blob/main/shell.json).

```qml
// 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`:

```qml
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`](https://github.com/basecamp/omarchy/blob/main/shell.json)) to define placement and settings:

```json
{
    "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:**

```qml
// 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:**

```qml
// 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`](https://github.com/basecamp/omarchy/blob/main/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`](https://github.com/basecamp/omarchy/blob/main/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`](https://github.com/basecamp/omarchy/blob/main/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.