sbar / docsSource ↗
sbar / Documentation

Text templates

Customise item labels with provider values and conditional sections.

Set an item’s text to replace its displayed label. Use a plain string for fixed text, or insert provider values with {{field}}. Omit text or set it to null to keep the existing presentation.

{
  "id": "cpu",
  "type": "cpu",
  "text": "CPU {{percentage}}%"
}

An empty string hides the text without hiding the item or removing its padding. Item and provider symbols and native application icons remain visible, except for throughput direction icons: those require a transfers loop with {{symbol}}. Groups, dividers, and spacers reject text. For a group, set templates on its children instead.

Values and sections

SyntaxResult
{{field}}Insert a value; a missing value becomes an empty string.
{{#field}}...{{/field}}Include the contents when the value is present.
{{^field}}...{{/field}}Include the contents when the value is missing, empty, or false.

Spaces around a tag are allowed, for example {{ percentage }}. Values are inserted as plain text, without HTML escaping or further template evaluation. An empty value or the literal value false counts as absent. Numeric zero counts as present, so a section around unreadCount also matches zero unread messages. Sections can nest up to eight levels. Presence sections test whether a value exists. Equality sections compare a value with an expected string.

Missing readings

Use available to avoid displaying an incomplete value while a reading is missing:

{
  "id": "memory",
  "type": "memory",
  "text": "{{#available}}RAM {{used}} / {{total}}{{/available}}{{^available}}RAM unavailable{{/available}}"
}

Optional metadata

Put a separator inside a section so it appears only with the corresponding metadata:

{
  "id": "media",
  "type": "media",
  "text": "{{#artist}}{{artist}}: {{/artist}}{{title}}{{^title}}Nothing playing{{/title}}"
}

Provider hide rules still apply. For example, media.hideWhenNotPlaying: true hides this item during paused, stopped, or unavailable playback even if the template produces text.

Keep the default label as a fallback

value contains the provider’s existing formatted label. Use it to preserve readable error states:

{
  "id": "mail",
  "type": "mail",
  "text": "{{#available}}Inbox: {{unreadCount}}{{/available}}{{^available}}{{value}}{{/available}}"
}

For date and time, keep using format, dateStyle, or timeStyle to format the date, then insert it with value:

{
  "id": "clock",
  "type": "datetime",
  "format": "HH:mm",
  "text": "Time {{value}}"
}

State-specific labels

Equality sections select a label for a particular state. Close them with the field name alone:

{
  "id": "vpn",
  "type": "vpn",
  "text": "{{#status=connected}}Secure{{/status}}{{^status=connected}}{{value}}{{/status}}"
}

{{#status=connected}} includes its contents when the status equals connected. {{^status=connected}} includes its contents when the status differs or is missing. The example keeps the default provider label for every other state. Sections can nest, including conditions on the same field.

Comparisons are case-sensitive string comparisons. Whitespace around the field, =, and expected value is ignored. Expected values are unquoted text, so {{#title=A song}}...{{/title}} matches that exact title. Numeric values compare their formatted strings; there are no arithmetic or ordering comparisons. Empty expected values are rejected; use {{^field}} to handle empty or missing fields. Boolean comparisons accept true and false. Media source comparisons accept Music and Spotify.

Allowed status values are validated for each provider. For example, {{#status=conected}} fails configuration validation rather than silently hiding text.

See the status values below for the accepted states.

Battery noBattery means no percentage reading, matching the existing AC power fallback. Otherwise charging takes precedence over plugged-in power, then battery power. Volume checks unavailable output first, then mute, then fixed volume with no percentage, then adjustable volume. Zero volume is still available.

Status follows the same item snapshot and source selection as the other fields. Network status respects interface filtering. VPN status is aggregate outside a service loop and describes the individual service inside it. Bluetooth status inside a device loop is connected.

Workspace fields and lists

Spaces, AeroSpace, and Yabai support the same workspace fields. Outside a loop, they describe the current workspace selected by the provider’s scope. Inside {{#workspaces}}...{{/workspaces}}, they describe each workspace in order.

{
  "id": "spaces",
  "type": "spaces",
  "text": "{{#workspaces}}{{#index=1}}Code{{/index}}{{^index=1}}{{name}}{{/index}}{{#separator}} · {{/separator}}{{/workspaces}}{{^workspaces}}{{value}}{{/workspaces}}",
  "spaces": { "scope": "display", "includeFullscreen": false }
}

The example renames the first Space to Code and retains per-Space highlighting. Use {{#separator}} · {{/separator}} inside the loop for a separator between entries. It uses the provider’s inactive tint and is never bold. Without an inactive tint it inherits the item colour. Ordinary literal text inside the loop still inherits that entry’s styling. No extra spacing is inserted between template runs. {{^workspaces}} supplies a fallback when the list is empty or unavailable.

FieldMeaning
nameNative display name; Spaces uses its one-based position, Yabai falls back to its Mission Control index
indexSpaces position after scope/fullscreen filtering; AeroSpace position in the scoped query order; Yabai’s original Mission Control index
workspaceIdNative Space/Yabai ID, or AeroSpace workspace name
totalNumber of entries after scope/fullscreen filtering
activeThe current workspace for this item’s scope
focusedThe globally focused workspace
visibleCurrently visible on a display
fullscreenNative fullscreen Space; always false for AeroSpace
availableWhether the current workspace is available
first, lastFirst/last entry in the source list, available inside a loop
valueDefault current label outside a loop; entry name inside it
idItem ID, including inside a loop

Spaces highlights the active entry. AeroSpace and Yabai emphasise the globally focused entry and retain their focused/visible/inactive tints. Hover tint still overrides all entry colours. Templates do not add per-workspace click actions.

includeFullscreen: false excludes fullscreen entries from loops. A current fullscreen Space still has fullscreen: true; its default value is Fullscreen. Native Spaces has no filtered index for that excluded entry. Yabai retains its native index and name. available can be true with an empty filtered list. Unavailable snapshots have no current fields, available: false, and total: 0.

For a current position and total, with the default label when index is missing, use {{#index}}{{index}} / {{total}}{{/index}}{{^index}}{{value}}{{/index}}. Scope selection and loops use the same captured snapshot as the provider’s default presentation. Reordering can change positional indexes; workspaceId identifies the source workspace instead.

VPN services and Bluetooth devices

VPN supports {{#services}}...{{/services}} and Bluetooth supports {{#devices}}...{{/devices}}. Each loop exposes the entry’s name and status for custom labels.

{
  "id": "vpn",
  "type": "vpn",
  "text": "{{#services}}{{name}}: {{#status=connected}}Secure{{/status}}{{^status=connected}}{{status}}{{/status}}{{#separator}}, {{/separator}}{{/services}}{{^services}}{{value}}{{/services}}"
}
{
  "id": "bluetooth",
  "type": "bluetooth",
  "text": "{{#devices}}{{name}} linked{{#separator}}, {{/separator}}{{/devices}}{{^devices}}{{value}}{{/devices}}"
}

services includes all monitored VPN services, including disconnected ones, when the service list is available. This differs from the default VPN value, which lists active services only. devices includes connected Bluetooth devices only. If Bluetooth is off, access is denied, or the provider is unavailable, the device collection is empty. Unavailable VPN service lists are empty too; stale entries are not rendered. Inverse sections supply a fallback for an empty list.

Entries sort by provider name, with their identifier breaking ties. Names collapse whitespace to one line; unnamed entries use VPN or Unnamed device. Values are inserted literally, without evaluating template syntax contained in a name.

FieldMeaning inside a loop
nameService or device name
statusIndividual VPN service status, or connected for a Bluetooth device
connectedWhether the entry is connected
availableVPN service status is not unavailable
serviceId, deviceIdProvider identifier, available for the corresponding collection
indexOne-based position in the sorted list
first, lastPosition flags for the source list
valueDefault entry label, such as Work connected
totalCollection count, also available outside the loop
idThe containing item ID

Outside a loop, status, connected, and value retain their aggregate meanings, as does VPN available. Bluetooth does not expose available. Entry-only fields are empty outside a loop.

VPN and Bluetooth loops produce plain text with the item’s aggregate symbol and tint. Native accessibility descriptions and hide rules remain intact. They use the same captured provider state as other template fields, including manual and interval snapshots. They do not trigger extra device scans or service queries.

Throughput fields and symbols

Use transfers to render download and upload with their native direction icons:

{
  "id": "throughput",
  "type": "throughput",
  "text": "{{#transfers}}{{symbol}}{{value}}{{#separator}} · {{/separator}}{{/transfers}}{{^transfers}}{{symbol}}{{value}}{{/transfers}}"
}

{{symbol}} requests the entry’s direction icon. Its placement follows the item’s symbolPosition; the tag’s position does not set icon placement. Use number for the scaled number alone, or unit for its suffix. Conditions on direction=download or direction=upload select a direction. See throughput templates for the field contexts and icon-only examples.

Collection rules

Collections cannot nest, be compared, or be inserted as scalar values. Each provider accepts only its own collection. {{#separator}}...{{/separator}} is valid only inside a collection, with no inverse, comparison, or scalar form. {{symbol}} is a shared value tag, valid inside or outside a collection. It cannot be used as a section or comparison.

Separators appear only between entries that render content. Entries that produce only a separator are omitted. Whitespace and symbol tags count as content, so keep literal spaces inside the condition when hiding an entry. Write spaces explicitly; templates add no spacing between runs.

first, last, index, and total still describe the source list. An inverse collection section tests whether that source list is empty, not whether conditions hid every entry. Multiple loops may appear beside one another; each renders independently.

Filter a list without stray separators

This shows connected VPN services, with commas only between visible names:

{
  "id": "vpn",
  "type": "vpn",
  "text": "{{#connected}}{{#services}}{{#connected}}{{name}}{{/connected}}{{#separator}}, {{/separator}}{{/services}}{{/connected}}{{^connected}}{{value}}{{/connected}}"
}

For source services Home disconnected, Office connected, and Work connected, the label is Office, Work. If Office disconnects, it becomes Work. If none are connected, the outer aggregate condition supplies the default label, such as VPN disconnected. It also preserves the default error label when an unavailable service takes precedence over connected services. A {{^services}} fallback would run only if the service list itself were empty.

The same rule applies to workspace lists. This shows visible workspaces and keeps the separator’s inactive colour:

{
  "id": "spaces",
  "type": "spaces",
  "text": "{{#workspaces}}{{#visible}}{{name}}{{/visible}}{{#separator}} · {{/separator}}{{/workspaces}}{{^workspaces}}{{value}}{{/workspaces}}"
}

Use the separator section for punctuation after filtering. A condition such as {{^last}}, {{/last}} uses the source position and can leave a trailing comma when the final source entry is hidden.

Provider fields

Every item that accepts text supports id, value, and symbol. id is the item’s configuration ID. value is the provider’s default label. symbol requests its native icon or the item’s configured symbol. Use the template to choose individual fields or text: "" to hide the label.

ProviderAdditional fields
Date and time-
Front applicationname
Batterypercentage, charging, pluggedIn, available, status
Volumepercentage, muted, available, status
Audio devicesname, status, available
MailunreadCount, status, available
Networkstatus, connected
VPNstatus, names, connected, available, services, name, serviceId, index, total, first, last, separator
Bluetoothstatus, names, count, connected, devices, name, deviceId, index, total, first, last, separator
CPUpercentage, available
Memoryused, total, percentage, usedBytes, totalBytes, available
Diskused, free, total, percentage, freeBytes, totalBytes, available
Throughputdownload, upload, download.value, download.unit, upload.value, upload.unit, available, transfers, direction, number, unit, separator, first, last, index, total
Mediatitle, artist, source, status, playing, available
Spacesworkspaces, separator, name, index, workspaceId, total, active, focused, visible, fullscreen, available, first, last
AeroSpaceworkspaces, separator, name, index, workspaceId, total, active, focused, visible, fullscreen, available, first, last
Yabaiworkspaces, separator, name, index, workspaceId, total, active, focused, visible, fullscreen, available, first, last
Shell command, Process pluginstatus, error
text, popup-

Percentages are rounded whole numbers without %; disk percentage measures used space. Memory and disk sizes include units, while fields ending in Bytes contain byte counts. Throughput rates include units and respect the configured bits/bytes setting. Use download.value and upload.value for scaled numbers without suffixes. Booleans render as true or false.

Media source is Music or Spotify. Its available field includes paused and stopped sources; use playing to test active playback. VPN names contains active service names; Bluetooth names contains connected device names and count is the device count. Both name lists are sorted and comma-separated.

Providerstatus values
Batterycharging, pluggedIn, onBattery, noBattery
Volumeavailable, muted, fixed, unavailable
Mediaplaying, paused, stopped, unknown
Mailavailable, closed, unauthorized, unavailable
Networkwifi, ethernet, cellular, other, offline
VPNconnecting, connected, disconnecting, disconnected, unavailable
Bluetoothon, off, connected, unauthorized, unavailable
Audio devicesavailable, disconnected, unavailable
Shell command, Process pluginrunning, success, failure

Fields follow the item’s media source, audio endpoint, disk path, network interface, and CPU/throughput smoothing settings. They also use its refresh snapshot. Editing the template changes the presentation of a held reading without refreshing it.

Symbols, visibility, and accessibility

{{symbol}} requests the provider’s native symbol or the item’s explicit symbol. It renders an icon, not a symbol name as text. The icon follows symbolPosition, regardless of where the tag appears. Repeated tags retain one aggregate icon. Throughput loops can render one direction icon per run.

Without a symbol tag, templates retain the provider’s normal icon behaviour. Once a template contains a symbol tag anywhere, the icon appears only if a branch containing that tag renders. Throughput direction icons always require a tag inside transfers when using an explicit template.

Show an icon conditionally

This shows the battery icon only while charging, with a fallback label when no percentage is available:

{
  "id": "battery",
  "type": "battery",
  "text": "{{#charging}}{{symbol}}{{/charging}}{{#available}}{{percentage}}%{{/available}}{{^available}}{{value}}{{/available}}",
  "symbolPosition": "right"
}

At 50% while charging, the text is 50% with the icon on the right. On battery power, the same text has no icon. Removing the symbol tag entirely restores the normal battery icon behaviour.

Provider showSymbol: false still hides symbols. A symbol tag does not create an icon for a provider that has none. Workspace, VPN, and Bluetooth loops retain one aggregate icon, even if several entries render the tag. Use provider state conditions around {{symbol}}; {{#symbol}} and {{#symbol=...}} are invalid.

Conditional application icons

Native application icons obey the same conditions and require frontApplication.showIcon: true:

{
  "id": "application",
  "type": "frontApplication",
  "text": "{{#name=Safari}}{{symbol}}{{/name}}{{name}}",
  "frontApplication": { "showIcon": true }
}

The application name always appears, but its native icon appears only for Safari. The configured item symbol remains the fallback if the native icon is unavailable.

Icon-only templates

Use {{symbol}} alone to request an icon without text:

{
  "id": "volume",
  "type": "volume",
  "text": "{{symbol}}"
}

An empty template also retains ordinary provider icons, but hides throughput direction icons. For those, use the directional icon-only example.

Workspace loops retain each entry’s colour and emphasis. Throughput transfers loops retain direction icons when they include {{symbol}}; scalar rate fields render text alone. An item-level symbol supplies one shared icon. See throughput visibility for symbol overrides and empty templates.

Item symbols, native application icons, item colours, and provider hide rules still apply. Native providers retain their existing accessibility descriptions. Command and plugin descriptions also retain their original presentation. Static, popup, application, and clock labels use the rendered text.

Validation and runtime changes

Unknown fields, unmatched tags, unclosed sections, and nesting beyond eight levels fail sbar validate. Collections used as values, nested collections, separator tags outside a collection, and symbol sections also fail validation. Fields must belong to the item’s type, so {{artist}} is invalid on a CPU item. A failed reload retains the previous valid configuration.

Edit text in the configuration file and let it reload. sbar set cannot change it. Templates affect displayed labels only; CLI values and subscription events keep their existing output. A text field returned by a command or plugin is result data, not a template.

Validate and try a template

Save a complete configuration and validate it before running the bar:

sbar validate --config "$PWD/config.json"
sbar start --config "$PWD/config.json"

The JSON item examples on this page belong inside items.left, items.center, or items.right. For example:

{
  "schemaVersion": 1,
  "bar": {},
  "items": {
    "right": [
      {
        "id": "cpu",
        "type": "cpu",
        "text": "{{#available}}{{symbol}}CPU {{percentage}}%{{/available}}{{^available}}{{value}}{{/available}}"
      }
    ]
  }
}

Check both sides of each condition on the Mac running the bar. Connect and disconnect a VPN service, switch Spaces, or change the foreground application to exercise the relevant example. For throughput, check active transfers, zero rates, and unavailable readings. Zero remains an available reading.

With manual refresh, the label and conditional icon use the held snapshot. Change the underlying state, then run sbar trigger <item-id> to capture it. Editing text alone renders the held values again. See refresh policies for interval and event behaviour.

Current limits

There are no custom number formatters, arbitrary command/plugin JSON field lookups, or escapes for literal {{.

Command and plugin value retains the existing truncation and error policy. Added template text is separate from that result. Date formatting remains controlled by the date/time options.

sbar documentation Built with Hugo · Reference reviewed 12 Sep 2026