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.
Basic Structure
Section titled “Basic Structure”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 QtQuickimport QtQuick.Layoutsimport Quickshellimport qs.Commonsimport 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 } }}Required Properties
Section titled “Required Properties”pluginApi
Section titled “pluginApi”Injected by the PluginService. Provides access to plugin APIs and services.
property var pluginApi: nullscreen
Section titled “screen”The ShellScreen this widget is displayed on (for multi-monitor support).
property ShellScreen screenwidgetId
Section titled “widgetId”Unique identifier for this widget instance.
property string widgetId: ""section
Section titled “section”The bar section this widget is in: "left", "center", or "right".
property string section: ""Sizing
Section titled “Sizing”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 capsuleHeightreadonly property real contentWidth: content.implicitWidth + Style.marginM * 2readonly property real contentHeight: capsuleHeight
// Click area matches content (bar loader may extend it)implicitWidth: contentWidthimplicitHeight: contentHeightFor vertical bar support (left/right positions):
// Per-screen bar propertiesreadonly 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 barsreadonly property real contentWidth: isBarVertical ? capsuleHeight : content.implicitWidth + Style.marginM * 2readonly property real contentHeight: isBarVertical ? content.implicitHeight + Style.marginM * 2 : capsuleHeight
implicitWidth: contentWidthimplicitHeight: contentHeightStyling
Section titled “Styling”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.
Colors
Section titled “Colors”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 }}Spacing and Sizes
Section titled “Spacing and Sizes”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}Typography
Section titled “Typography”// Define per-screen font size at root levelreadonly 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}Using Settings
Section titled “Using Settings”Access plugin settings via pluginApi:
// Get setting with fallback to defaultreadonly property string message: pluginApi?.pluginSettings?.message || pluginApi?.manifest?.metadata?.defaultSettings?.message || "Default Message"
// Update and save settingfunction updateMessage(newMessage) { pluginApi.pluginSettings.message = newMessage pluginApi.saveSettings()}Interactions
Section titled “Interactions”Click Events
Section titled “Click Events”MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor
onClicked: { // Open plugin panel near this widget pluginApi.openPanel(root.screen, root)
// Or perform action performAction() }}Hover Effects
Section titled “Hover Effects”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 }}Tooltips
Section titled “Tooltips”import qs.Services.UIimport 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).
Context Menus
Section titled “Context Menus”Add a right-click context menu to your widget using NPopupContextMenu and PanelService:
import qs.Services.UIimport 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); } } }}Context Menu Model
Section titled “Context Menu Model”Each item in the model array is an object with these properties:
| Property | Type | Description |
|---|---|---|
label | string | Display text for the menu item |
action | string | Action identifier passed to onTriggered |
icon | string | Tabler icon name (optional) |
enabled | bool | Whether the item is clickable (default: true) |
model: [ { "label": "Active Item", "action": "active", "icon": "check" }, { "label": "Disabled Item", "action": "disabled", "icon": "x", "enabled": false }]Using NIconButton
Section titled “Using NIconButton”If your widget extends NIconButton, use the built-in onRightClicked signal:
import qs.Services.UIimport 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.).
Opening Panels
Section titled “Opening Panels”If your plugin provides a panel, open it from the widget:
MouseArea { anchors.fill: parent onClicked: { if (pluginApi) { pluginApi.openPanel(root.screen, root) } }}Using Noctalia Widgets
Section titled “Using Noctalia Widgets”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}Buttons
Section titled “Buttons”NIconButton { icon: "settings" onClicked: { // Handle click }}Accessing Services
Section titled “Accessing Services”Plugins can use Noctalia services:
Toast Notifications
Section titled “Toast Notifications”import qs.Services.UI
ToastService.showNotice("Success message")ToastService.showError("Error message")Logging
Section titled “Logging”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")}Settings Access
Section titled “Settings Access”import qs.Commons
// Access global Noctalia settingsreadonly property string barPosition: Settings.data.bar.positionreadonly property bool isDarkMode: Settings.data.ui.darkModeComplete Examples
Section titled “Complete Examples”Simple Status Widget
Section titled “Simple Status Widget”import QtQuickimport QtQuick.Layoutsimport Quickshellimport qs.Commonsimport 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 } } }}Interactive Counter Widget
Section titled “Interactive Counter Widget”import QtQuickimport QtQuick.Layoutsimport Quickshellimport qs.Commonsimport qs.Widgetsimport 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) } }}Widget with Panel
Section titled “Widget with Panel”import QtQuickimport QtQuick.Layoutsimport Quickshellimport qs.Commonsimport 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) } } }}Vertical Bar Support
Section titled “Vertical Bar Support”Support vertical bars (left/right positions):
import QtQuickimport QtQuick.Layoutsimport Quickshellimport qs.Commonsimport 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 } }}Best Practices
Section titled “Best Practices”- Use the visual capsule pattern:
Itemroot with centeredRectanglevisualCapsule for extended click areas - Keep it small: Bar widgets should be compact and unobtrusive
- Use consistent styling: Stick to Noctalia’s design system
- Use per-screen properties: Always use
Style.getCapsuleHeightForScreen(screenName)andStyle.getBarFontSizeForScreen(screenName)instead of global values - Handle missing data: Always provide fallbacks for settings and data
- Respect dark mode: Use
Color.m*colors that adapt to theme - Use binding for hover effects:
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor - Log important events: Use Logger for debugging
- Test on vertical bars: Ensure your widget works in all bar positions
- Optimize performance: Avoid expensive operations in the widget
See Also
Section titled “See Also”- Getting Started - Create your first plugin
- Desktop Widget Development - Create desktop widgets
- Panel Development - Create overlay panels
- Plugin API - Full API reference
- Manifest Reference - Plugin configuration