Skip to content

Styling

locainin edited this page May 31, 2026 · 8 revisions

Styling

UnixNotis uses GTK4 CSS. Theme files live in the config directory and are hot-reloaded when edited.

The theme path is additive:

  • existing themes work through the legacy GTK color path
  • GTK runtimes with custom property support can use var(...)
  • css-check understands the modern path instead of treating it like blanket bad input

Theme authors can keep writing legacy-safe CSS or use modern GTK CSS features for cleaner, more reusable theme files.

CSS files

Theme files are stored under $XDG_CONFIG_HOME/unixnotis:

  • base.css: palette and shared tokens
  • panel.css: control-center panel rules
  • popup.css: toast popup rules
  • widgets.css: toggle, stat, card, and shared widget rules
  • media.css: media-widget-only rules loaded above widgets.css

If a file is missing, a default template is created on startup.

Load order

Panel UI:

  1. base.css
  2. panel.css
  3. widgets.css
  4. media.css

Popup UI:

  1. base.css
  2. popup.css

Later files override earlier rules.

Theme tokens

base.css defines the palette and theme hooks. Adjust these first to re-skin the UI without touching component rules.

Common tokens:

  • unixnotis-surface-base, unixnotis-card-base
  • unixnotis-text, unixnotis-muted, unixnotis-accent
  • unixnotis-panel-grad-1/2/3
  • unixnotis-notification-bg-1/2

Modern GTK CSS support

UnixNotis supports modern GTK CSS on runtimes that can parse it, while keeping the legacy theme path intact for existing configs.

That adds three real benefits:

  1. Reusable values
  • Custom properties make it possible to define a size or color once and reuse it across panel cards, popups, toggles, media rows, and custom selectors
  1. Cleaner math
  • calc(...) can be used for GTK size math when the property accepts it, which makes spacing and sizing easier to tune without hardcoding every number
  1. More stable theme targets
  • The UI exposes shared hook classes for panel cards, popup cards, and media cards, so themes can react to widget state directly instead of guessing from text or fragile selector chains

The goal is to support modern GTK styling without breaking existing themes.

Legacy path vs modern path

Legacy-safe path:

  • @define-color
  • stock class names
  • direct fixed values like 12px, 16px, alpha(...)

Modern additive path:

  • :root { --unixnotis-* }
  • var(--unixnotis-...)
  • calc(...)
  • newer shared hook classes on panel, popup, and media widgets

Existing configs do not need to change. The modern path is there for themes that want it.

Config-driven theme knobs

The [theme] section in config.toml controls alpha and geometry values applied at runtime:

[theme]
border_width = 1
card_radius = 16
surface_alpha = 0.88
surface_strong_alpha = 0.96
card_alpha = 0.94
shadow_soft_alpha = 0.30
shadow_strong_alpha = 0.55

Those config values feed both:

  • the legacy color override layer
  • the --unixnotis-* custom-property layer on supported GTK runtimes

That matters because one config knob can drive both theme paths without forcing users to rewrite their CSS.

Shared custom properties

When the GTK runtime supports custom properties, UnixNotis emits shared --unixnotis-* values that themes can consume directly.

Examples:

  • --unixnotis-border-width
  • --unixnotis-card-radius
  • --unixnotis-card-alpha
  • --unixnotis-panel-header-radius
  • --unixnotis-panel-header-padding
  • --unixnotis-panel-card-padding-y
  • --unixnotis-panel-card-padding-x
  • --unixnotis-panel-search-min-height
  • --unixnotis-panel-search-padding-x
  • --unixnotis-notification-card-radius
  • --unixnotis-notification-action-padding-y
  • --unixnotis-notification-action-padding-x
  • --unixnotis-popup-card-padding-y
  • --unixnotis-popup-card-padding-x
  • --unixnotis-toggle-min-width
  • --unixnotis-toggle-min-height
  • --unixnotis-stat-card-radius
  • --unixnotis-stat-card-padding-y
  • --unixnotis-stat-card-padding-x
  • --unixnotis-info-card-radius
  • --unixnotis-info-card-min-height
  • --unixnotis-calendar-radius
  • --unixnotis-media-art-size
  • --unixnotis-media-row-gap
  • --unixnotis-accent-color
  • --unixnotis-card-color

These values are useful because themes can keep stock layout numbers in one place instead of duplicating them across many rules.

Reusable theme values

Reuse one value in many places:

:root {
  --card-gap: 14px;
}

.unixnotis-panel-card {
  padding: var(--unixnotis-panel-card-padding-y) var(--unixnotis-panel-card-padding-x);
  border-radius: var(--unixnotis-card-radius);
}

.unixnotis-popup-card {
  padding: calc(var(--unixnotis-popup-card-padding-y) + 2px)
           var(--unixnotis-popup-card-padding-x);
}

.unixnotis-media-card {
  gap: calc(var(--card-gap) - 4px);
}

This keeps repeated sizes in one place and makes css-check output easier to reason about.

Common selectors

Panel shell and header:

  • .unixnotis-panel
  • .unixnotis-panel-header
  • .unixnotis-panel-header-top
  • .unixnotis-panel-title
  • .unixnotis-panel-subtitle
  • .unixnotis-panel-count
  • .unixnotis-panel-body-stack
  • .unixnotis-panel-edge-top, .unixnotis-panel-edge-bottom
  • .unixnotis-panel-rail-left, .unixnotis-panel-rail-right
  • .unixnotis-panel-search
  • .unixnotis-panel-search-shell
  • .unixnotis-panel-search-accent
  • .unixnotis-panel-search-star
  • .unixnotis-panel-action
  • .unixnotis-section-header
  • .unixnotis-recent-section
  • .unixnotis-recent-header-row
  • .unixnotis-recent-header
  • .unixnotis-panel-footer

Notification list:

  • .unixnotis-group, .unixnotis-group-header
  • .unixnotis-panel-card
  • .unixnotis-panel-card-grouped
  • .unixnotis-panel-card-group-collapsed
  • .unixnotis-panel-card-group-expanded
  • .unixnotis-stack-ghost

Popup cards:

  • .unixnotis-popup-card
  • .unixnotis-popup-actions button

Empty state (no notifications):

  • .unixnotis-empty
  • .unixnotis-empty-label

Widgets:

  • .unixnotis-quick-controls
  • .unixnotis-quick-slider
  • .unixnotis-toggle
  • .unixnotis-toggle-kind-<kind> (added when a toggle kind is set)
  • .unixnotis-stat-card
  • .unixnotis-info-card
  • .unixnotis-media-card
  • .unixnotis-media-stack
  • .unixnotis-media-row
  • .unixnotis-media-header
  • .unixnotis-media-body
  • .unixnotis-media-text
  • .unixnotis-media-art-frame
  • .unixnotis-media-nav-strip
  • .unixnotis-media-button-play

Shared hook classes

UnixNotis exposes stable class hooks for real widget state.

Panel shell and panel action hooks:

  • .unixnotis-panel-window
  • .unixnotis-panel
  • .unixnotis-panel-header
  • .unixnotis-panel-header-top
  • .unixnotis-panel-title-stack
  • .unixnotis-panel-title-row
  • .unixnotis-panel-title
  • .unixnotis-panel-subtitle
  • .unixnotis-panel-count
  • .unixnotis-panel-search
  • .unixnotis-panel-search-revealer
  • .unixnotis-media-container
  • .unixnotis-quick-controls
  • .unixnotis-widget-stack
  • .unixnotis-widget-revealer
  • .unixnotis-section-header
  • .unixnotis-recent-section
  • .unixnotis-recent-header
  • .unixnotis-recent-header-row
  • .unixnotis-panel-footer
  • .unixnotis-toggle-section
  • .unixnotis-stat-section
  • .unixnotis-card-section
  • .unixnotis-panel-actions
  • .unixnotis-panel-action-group
  • .unixnotis-panel-action
  • .unixnotis-panel-action-content
  • .unixnotis-panel-action-glyph
  • .unixnotis-panel-action-label
  • .unixnotis-panel-action-label-hidden
  • .unixnotis-panel-action-focus
  • .unixnotis-panel-action-primary
  • .unixnotis-panel-action-muted
  • .unixnotis-panel-action-search
  • .unixnotis-panel-action-close
  • .unixnotis-panel-action-with-icon
  • .unixnotis-panel-action-icon

Panel card hooks:

  • .unixnotis-panel-card-header
  • .unixnotis-panel-card-text
  • .unixnotis-panel-card-meta-top
  • .unixnotis-panel-card-meta-label
  • .unixnotis-panel-card-time-badge
  • .unixnotis-panel-card-footer
  • .unixnotis-panel-card-footer-left
  • .unixnotis-panel-card-footer-right
  • .unixnotis-panel-card-thumbnail
  • .unixnotis-panel-card-has-actions
  • .unixnotis-panel-card-no-actions
  • .unixnotis-panel-card-has-body
  • .unixnotis-panel-card-has-summary
  • .unixnotis-panel-card-has-thumbnail
  • .unixnotis-panel-card-no-thumbnail

Slider hooks:

  • .unixnotis-quick-slider-stack
  • .unixnotis-quick-slider-segments
  • .unixnotis-quick-slider-segment
  • .unixnotis-quick-slider-sublabel-row
  • .unixnotis-quick-slider-sublabel-min
  • .unixnotis-quick-slider-sublabel-max

Info card layout hooks:

  • .unixnotis-info-card-banner
  • .unixnotis-info-card-image-row
  • .unixnotis-info-media
  • .unixnotis-info-chrome
  • .unixnotis-info-dots
  • .unixnotis-info-dot
  • .unixnotis-info-nav-prev
  • .unixnotis-info-nav-next

Toggle hooks:

  • .unixnotis-toggle-grid
  • .unixnotis-toggle
  • .unixnotis-toggle-content
  • .unixnotis-toggle-icon
  • .unixnotis-toggle-label
  • .unixnotis-toggle-has-icon
  • .unixnotis-toggle-no-icon

Stat hooks:

  • .unixnotis-stat-grid
  • .unixnotis-stat-card
  • .unixnotis-stat-header
  • .unixnotis-stat-icon
  • .unixnotis-stat-title
  • .unixnotis-stat-value
  • .unixnotis-stat-card-builtin
  • .unixnotis-stat-card-plugin
  • .unixnotis-stat-card-has-icon
  • .unixnotis-stat-card-no-icon

Info card hooks:

  • .unixnotis-card-grid
  • .unixnotis-info-card
  • .unixnotis-info-header
  • .unixnotis-info-icon
  • .unixnotis-info-title
  • .unixnotis-info-body
  • .unixnotis-calendar
  • .unixnotis-info-card-calendar
  • .unixnotis-info-card-weather
  • .unixnotis-info-card-mono
  • .unixnotis-info-card-has-icon
  • .unixnotis-info-card-no-icon

Popup card hooks:

  • .unixnotis-popup-card-has-actions
  • .unixnotis-popup-card-has-body
  • .unixnotis-popup-card-has-icon
  • .unixnotis-popup-card-no-icon
  • .unixnotis-popup-card-has-summary

Media card hooks:

  • .unixnotis-media-card-has-art
  • .unixnotis-media-card-no-art
  • .unixnotis-media-card-has-artist
  • .unixnotis-media-card-empty-artist
  • .unixnotis-media-card-playing
  • .unixnotis-media-card-paused
  • .unixnotis-media-card-stopped
  • .unixnotis-media-card-single-player
  • .unixnotis-media-card-multi-player

Media shell hooks:

  • .unixnotis-media-stack
  • .unixnotis-media-stack-player
  • .unixnotis-media-row
  • .unixnotis-media-row-player
  • .unixnotis-media-header
  • .unixnotis-media-body
  • .unixnotis-media-text
  • .unixnotis-media-main
  • .unixnotis-media-meta
  • .unixnotis-media-source
  • .unixnotis-media-position
  • .unixnotis-media-title
  • .unixnotis-media-artist
  • .unixnotis-media-art
  • .unixnotis-media-art-frame
  • .unixnotis-media-controls
  • .unixnotis-media-control-strip
  • .unixnotis-media-action-rail
  • .unixnotis-media-nav-strip
  • .unixnotis-media-nav
  • .unixnotis-media-nav-prev
  • .unixnotis-media-nav-next
  • .unixnotis-media-button
  • .unixnotis-media-button-prev
  • .unixnotis-media-button-play
  • .unixnotis-media-button-next
  • .unixnotis-media-card-player
  • .unixnotis-media-has-title
  • .unixnotis-media-no-title
  • .unixnotis-media-has-source
  • .unixnotis-media-no-source
  • .unixnotis-media-has-position
  • .unixnotis-media-no-position
  • .unixnotis-media-has-controls
  • .unixnotis-media-no-controls
  • .unixnotis-media-has-nav
  • .unixnotis-media-no-nav
  • .unixnotis-media-art-start
  • .unixnotis-media-art-top
  • .unixnotis-media-art-hidden
  • .unixnotis-media-controls-inline
  • .unixnotis-media-controls-bottom
  • .unixnotis-media-controls-side
  • .unixnotis-media-controls-hidden
  • .unixnotis-media-nav-external
  • .unixnotis-media-nav-inline
  • .unixnotis-media-nav-bottom
  • .unixnotis-media-nav-side
  • .unixnotis-media-nav-hidden

Grouped row hooks:

  • .unixnotis-group
  • .unixnotis-group-row
  • .unixnotis-group-header
  • .unixnotis-group-icon
  • .unixnotis-group-title
  • .unixnotis-group-count
  • .unixnotis-group-chevron
  • .unixnotis-group-row-collapsed
  • .unixnotis-group-row-expanded
  • .unixnotis-group-row-has-icon
  • .unixnotis-group-row-no-icon

Grouped notification card hooks:

  • .unixnotis-panel-card-grouped
  • .unixnotis-panel-card-group-collapsed
  • .unixnotis-panel-card-group-expanded

Placeholder row hooks:

  • .unixnotis-empty
  • .unixnotis-empty-label
  • .unixnotis-stack-ghost
  • .unixnotis-stack-ghost-<depth>

Notification stack notes:

  • stack ghosts are decoration inside the notification row, not separate notification records
  • .unixnotis-stack-ghost-1 is the first depth layer and appears when a group has at least two notifications
  • .unixnotis-stack-ghost-2 is the deeper layer and appears when a group has at least three notifications
  • keep ghost min-height, top margin, and bottom margin compact for an iOS-like card depth
  • large ghost heights make collapsed groups look like empty cards instead of stacked cards

Shared state hooks:

  • .active
  • .critical
  • .empty
  • .playing
  • .stacked

These hooks are one of the biggest customization gains in this update. They let themes react to real UI state directly.

Example:

.unixnotis-media-card.unixnotis-media-card-no-art {
  padding-left: calc(var(--unixnotis-media-card-padding-x) + 6px);
}

.unixnotis-media-card.unixnotis-media-card-multi-player .unixnotis-media-position {
  color: var(--unixnotis-accent-color);
}

.unixnotis-media-art-top .unixnotis-media-art-frame {
  min-width: calc(var(--unixnotis-media-art-size) + 10px);
}

.unixnotis-media-controls-bottom .unixnotis-media-control-strip {
  padding-top: 4px;
}

.unixnotis-media-no-source .unixnotis-media-position {
  background: alpha(var(--unixnotis-card-color), 0.32);
}

.unixnotis-popup-card.unixnotis-popup-card-no-icon {
  padding-left: calc(var(--unixnotis-popup-card-padding-x) + 8px);
}

Empty state layout

The empty-state text is configurable in two ways:

  • panel.empty_text controls the label text.
  • panel.empty_offset_top shifts the label down when widgets are visible.

When no widgets are visible, the empty state is centered in the list area.

Example:

.unixnotis-empty-label {
  letter-spacing: 0.35em;
  font-size: 12px;
  text-transform: uppercase;
  color: @unixnotis-muted;
}

Widget styling examples

.unixnotis-quick-slider {
  border-radius: var(--unixnotis-card-radius);
  min-height: calc(var(--unixnotis-toggle-min-width) / 2);
}

.unixnotis-toggle:checked {
  background: alpha(@unixnotis-accent, 0.25);
}

.unixnotis-toggle.unixnotis-toggle-kind-wifi:checked {
  background: alpha(@unixnotis-accent, 0.25);
}

.unixnotis-toggle.unixnotis-toggle-kind-bluetooth:checked {
  background: alpha(@unixnotis-accent-2, 0.25);
}

.unixnotis-panel-card.unixnotis-panel-card-has-summary {
  padding-bottom: calc(var(--unixnotis-panel-card-padding-y) + 2px);
}

.unixnotis-media-card.unixnotis-media-card-playing {
  border-color: var(--unixnotis-accent-color);
}

.unixnotis-stat-value {
  font-weight: 700;
  letter-spacing: 0.03em;
}

css-check and theming

noticenterctl css-check validates theme syntax, theme wiring, and common layout risks.

It helps with three different theme-authoring problems:

  1. Syntax and parser failures
  • broken GTK CSS
  1. Theme wiring problems
  • missing active files
  • duplicate theme slots
  • outside-root theme asset issues
  1. Layout pressure
  • rules that are likely to force the panel wider than expected
  • tracked widget sizing that looks unsafe for the configured panel width
  • descendant selectors that end on a UnixNotis hook and carry size rules

It also understands modern GTK CSS. Valid var(...) and calc(...) usage is not treated as a blanket warning just because it looks more web-like. Size rules aimed at GTK subnodes, such as slider trough, stay quiet unless the final selector target is a UnixNotis hook.

Reload behavior

CSS files are watched for changes and reloaded on write. Large edits are coalesced into a single reload to avoid flicker.

Clone this wiki locally