Skip to content

Bar Widget

Bar widgets are components that appear in the Noctalia bar (top, bottom, left, or right). They provide quick access to information and actions.

A bar widget is a QML file that must follow this structure. The pattern uses an Item root with a centered visualCapsule Rectangle to ensure click areas extend to the full bar height while keeping the visual content properly sized:

import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
Item {
id: root
// Plugin API (injected by PluginService)
property var pluginApi: null
// Required properties for bar widgets
property ShellScreen screen
property string widgetId: ""
property string section: ""
// Per-screen bar properties (for multi-monitor and vertical bar support)
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
// Content dimensions (visual capsule size)
readonly property real contentWidth: content.implicitWidth + Style.marginM * 2
readonly property real contentHeight: capsuleHeight
// Widget dimensions (extends to full bar height for better click area)
implicitWidth: contentWidth
implicitHeight: contentHeight
// Visual capsule - centered within the full click area
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
radius: Style.radiusL
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// Your widget content here (centered in visualCapsule)
RowLayout {
id: content
anchors.centerIn: parent
spacing: Style.marginS
// ... content items
}
}
// MouseArea at root level for extended click area
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Handle click
}
}
}

Injected by the PluginService. Provides access to plugin APIs and services.

property var pluginApi: null

The ShellScreen this widget is displayed on (for multi-monitor support).

property ShellScreen screen

Unique identifier for this widget instance.

property string widgetId: ""

The bar section this widget is in: "left", "center", or "right".

property string section: ""

Bar widgets use contentWidth and contentHeight properties to define the visual capsule size, while implicitWidth and implicitHeight control the click area. Use per-screen properties to support multi-monitor setups and per-screen bar settings:

// Per-screen bar properties (required for proper multi-monitor support)
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
// Define visual content size using per-screen capsuleHeight
readonly property real contentWidth: content.implicitWidth + Style.marginM * 2
readonly property real contentHeight: capsuleHeight
// Click area matches content (bar loader may extend it)
implicitWidth: contentWidth
implicitHeight: contentHeight

For vertical bar support (left/right positions):

// Per-screen bar properties
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
// Swap width/height for vertical bars
readonly property real contentWidth: isBarVertical ? capsuleHeight : content.implicitWidth + Style.marginM * 2
readonly property real contentHeight: isBarVertical ? content.implicitHeight + Style.marginM * 2 : capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight

Use Noctalia’s built-in styling system for consistency:

Required Styling

Bar widgets must have a background and an outline for proper visual integration with the bar. Apply these to the visualCapsule Rectangle:

Rectangle {
id: visualCapsule
// Required: Background color with hover support
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
// Required: Outline border
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// Required: Rounded corners
radius: Style.radiusL
}

Without these properties, the widget may not display correctly or may not integrate properly with the bar’s visual style.

import qs.Commons
Rectangle {
id: visualCapsule
// Surface colors (for visualCapsule background)
color: Color.mSurface
color: Color.mSurfaceVariant
color: Style.capsuleColor // Recommended for bar widgets
// Text colors
NText {
color: Color.mOnSurface
color: Color.mOnSurfaceVariant
color: Color.mPrimary
}
}
import qs.Commons
ColumnLayout {
spacing: Style.marginS // Small spacing
spacing: Style.marginM // Medium spacing
spacing: Style.marginL // Large spacing
NIcon {
pointSize: Style.fontSizeS // Small icon
pointSize: Style.fontSizeM // Medium icon
pointSize: Style.fontSizeL // Large icon
}
}
Rectangle {
radius: Style.radiusS // Small radius
radius: Style.radiusM // Medium radius (recommended)
radius: Style.radiusL // Large radius
}
// Define per-screen font size at root level
readonly property string screenName: screen?.name ?? ""
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
NText {
// For bar widgets, use per-screen barFontSize for consistent sizing
pointSize: barFontSize // Recommended for bar widget text
// Other available sizes (for panels, dialogs, etc.)
pointSize: Style.fontSizeXS
pointSize: Style.fontSizeS
pointSize: Style.fontSizeM
pointSize: Style.fontSizeL
font.weight: Font.Light
font.weight: Font.Normal
font.weight: Font.Medium
font.weight: Font.Bold
}

Access plugin settings via pluginApi:

// Get setting with fallback to default
readonly property string message:
pluginApi?.pluginSettings?.message ||
pluginApi?.manifest?.metadata?.defaultSettings?.message ||
"Default Message"
// Update and save setting
function updateMessage(newMessage) {
pluginApi.pluginSettings.message = newMessage
pluginApi.saveSettings()
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
// Open plugin panel near this widget
pluginApi.openPanel(root.screen, root)
// Or perform action
performAction()
}
}

Use a property binding on the visualCapsule color instead of onEntered/onExited handlers for cleaner, more reliable hover effects:

Rectangle {
id: visualCapsule
// ... other properties
// Hover effect via binding - cleaner than onEntered/onExited
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Handle click
}
}
import qs.Services.UI
import qs.Services.System
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
TooltipService.show(root, "Widget tooltip text", BarService.getTooltipDirection())
}
onExited: {
TooltipService.hide()
}
}

You can also use a function to build dynamic tooltip content:

function buildTooltip() {
return "Status: " + (enabled ? "Active" : "Inactive")
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
TooltipService.show(root, buildTooltip(), BarService.getTooltipDirection())
}
onExited: {
TooltipService.hide()
}
}

The BarService.getTooltipDirection() function automatically determines the correct tooltip direction based on the bar’s position (top, bottom, left, or right).

Add a right-click context menu to your widget using NPopupContextMenu and PanelService:

import qs.Services.UI
import qs.Widgets
Item {
id: root
property var pluginApi: null
property ShellScreen screen
// ... other properties
// Per-screen bar properties
readonly property string screenName: screen?.name ?? ""
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real contentWidth: content.implicitWidth + Style.marginM * 2
readonly property real contentHeight: capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
radius: Style.radiusL
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
RowLayout {
id: content
anchors.centerIn: parent
// ... content
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": pluginApi?.tr("menu.refresh") || "Refresh",
"action": "refresh",
"icon": "refresh"
},
{
"label": pluginApi?.tr("menu.settings") || "Settings",
"action": "settings",
"icon": "settings"
}
]
onTriggered: action => {
// Always close the menu first
contextMenu.close();
PanelService.closeContextMenu(screen);
// Handle actions
if (action === "refresh") {
// Perform refresh
} else if (action === "settings") {
BarService.openPluginSettings(screen, pluginApi.manifest);
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
pluginApi?.openPanel(root.screen, root)
} else if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
}
}

Each item in the model array is an object with these properties:

PropertyTypeDescription
labelstringDisplay text for the menu item
actionstringAction identifier passed to onTriggered
iconstringTabler icon name (optional)
enabledboolWhether the item is clickable (default: true)
model: [
{ "label": "Active Item", "action": "active", "icon": "check" },
{ "label": "Disabled Item", "action": "disabled", "icon": "x", "enabled": false }
]

If your widget extends NIconButton, use the built-in onRightClicked signal:

import qs.Services.UI
import qs.Widgets
NIconButton {
id: root
property var pluginApi: null
property ShellScreen screen
icon: "my-icon"
onClicked: {
pluginApi?.openPanel(root.screen, root)
}
onRightClicked: {
PanelService.showContextMenu(contextMenu, root, screen);
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
"icon": "settings"
}
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "widget-settings") {
BarService.openPluginSettings(screen, pluginApi.manifest);
}
}
}
}

Required Pattern

Always use PanelService.showContextMenu() to open context menus and call both contextMenu.close() and PanelService.closeContextMenu(screen) in the onTriggered handler. This ensures proper behavior across all Wayland compositors (Hyprland, Sway, Niri, labwc, etc.).

If your plugin provides a panel, open it from the widget:

MouseArea {
anchors.fill: parent
onClicked: {
if (pluginApi) {
pluginApi.openPanel(root.screen, root)
}
}
}

Noctalia provides many pre-built widgets:

import qs.Widgets
NIcon {
icon: "heart" // Tabler icon name
color: Color.mPrimary
applyUiScale: true // Automatically scale with UI
}
NText {
text: "Hello World"
color: Color.mOnSurface
pointSize: barFontSize // Use per-screen barFontSize for bar widgets
font.weight: Font.Medium
}
NIconButton {
icon: "settings"
onClicked: {
// Handle click
}
}

Plugins can use Noctalia services:

import qs.Services.UI
ToastService.showNotice("Success message")
ToastService.showError("Error message")
import qs.Commons
Component.onCompleted: {
Logger.i("MyPlugin", "Widget loaded")
Logger.d("MyPlugin", "Debug info:", someValue)
Logger.w("MyPlugin", "Warning message")
Logger.e("MyPlugin", "Error occurred")
}
import qs.Commons
// Access global Noctalia settings
readonly property string barPosition: Settings.data.bar.position
readonly property bool isDarkMode: Settings.data.ui.darkMode
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
Item {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
// Per-screen bar properties
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
readonly property real contentWidth: row.implicitWidth + Style.marginM * 2
readonly property real contentHeight: capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
color: Style.capsuleColor
radius: Style.radiusL
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
RowLayout {
id: row
anchors.centerIn: parent
spacing: Style.marginS
NIcon {
icon: "check"
color: Color.mPrimary
}
NText {
text: "Ready"
color: Color.mOnSurface
pointSize: barFontSize
}
}
}
}
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
import qs.Services.UI
Item {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
// Per-screen bar properties
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
property int count: pluginApi?.pluginSettings?.count || 0
readonly property real contentWidth: row.implicitWidth + Style.marginM * 2
readonly property real contentHeight: capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
radius: Style.radiusL
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
RowLayout {
id: row
anchors.centerIn: parent
spacing: Style.marginS
NIcon {
icon: "numbers"
color: Color.mPrimary
}
NText {
text: root.count.toString()
color: Color.mOnSurface
pointSize: barFontSize
font.weight: Font.Bold
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.count++
pluginApi.pluginSettings.count = root.count
pluginApi.saveSettings()
ToastService.showNotice("Count: " + root.count)
}
}
}
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
Item {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
// Per-screen bar properties
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
readonly property string message:
pluginApi?.pluginSettings?.message ||
pluginApi?.manifest?.metadata?.defaultSettings?.message || ""
readonly property real contentWidth: row.implicitWidth + Style.marginM * 2
readonly property real contentHeight: capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
radius: Style.radiusL
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
RowLayout {
id: row
anchors.centerIn: parent
spacing: Style.marginS
NIcon {
icon: "noctalia"
}
NText {
text: root.message
color: Color.mOnSurface
pointSize: barFontSize
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (pluginApi) {
pluginApi.openPanel(root.screen, root)
}
}
}
}

Support vertical bars (left/right positions):

import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
Item {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
// Per-screen bar properties
readonly property string screenName: screen?.name ?? ""
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
readonly property real contentWidth: isBarVertical ? capsuleHeight : layout.implicitWidth + Style.marginM * 2
readonly property real contentHeight: isBarVertical ? layout.implicitHeight + Style.marginM * 2 : capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
radius: Style.radiusL
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Item {
id: layout
anchors.centerIn: parent
implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth
implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight
RowLayout {
id: rowLayout
visible: !root.isBarVertical
spacing: Style.marginS
NIcon {
icon: "heart"
color: Color.mPrimary
}
NText {
text: "Widget"
color: Color.mOnSurface
pointSize: barFontSize
}
}
ColumnLayout {
id: colLayout
visible: root.isBarVertical
spacing: Style.marginS
NIcon {
icon: "heart"
color: Color.mPrimary
}
NText {
text: "Widget"
color: Color.mOnSurface
pointSize: barFontSize
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Handle click
}
}
}
  1. Use the visual capsule pattern: Item root with centered Rectangle visualCapsule for extended click areas
  2. Keep it small: Bar widgets should be compact and unobtrusive
  3. Use consistent styling: Stick to Noctalia’s design system
  4. Use per-screen properties: Always use Style.getCapsuleHeightForScreen(screenName) and Style.getBarFontSizeForScreen(screenName) instead of global values
  5. Handle missing data: Always provide fallbacks for settings and data
  6. Respect dark mode: Use Color.m* colors that adapt to theme
  7. Use binding for hover effects: color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
  8. Log important events: Use Logger for debugging
  9. Test on vertical bars: Ensure your widget works in all bar positions
  10. Optimize performance: Avoid expensive operations in the widget