Skip to content

Styling

locainin edited this page Apr 17, 2026 · 6 revisions

Styling

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

The important part is that the theme path is now additive:

  • old themes still work through the legacy GTK color path
  • newer GTK setups can also use modern custom properties and var(...)
  • css-check understands that newer path instead of treating it like blanket bad input

This means theme authors can keep writing safe legacy CSS, or opt into newer GTK CSS features for cleaner and 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 now supports a newer GTK CSS path on runtimes that can parse it, while keeping the old 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 now exposes more 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 more of what newer GTK already supports, 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 now feed both:

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

That matters because a single config knob can now drive both old and new theme styles 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 no longer have to duplicate the same stock layout numbers in many different rules just to stay aligned with the live UI.

What can be done now that the old path did not support cleanly

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);
}

Before this update, that style was either unsupported, noisy in css-check, or much harder to reason about because the checker could not follow those values correctly.

Common selectors

Panel shell and header:

  • .unixnotis-panel
  • .unixnotis-panel-header
  • .unixnotis-panel-title
  • .unixnotis-panel-action

Notification list:

  • .unixnotis-group, .unixnotis-group-header
  • .unixnotis-panel-card
  • .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 now exposes more stable class hooks for real widget state.

Panel shell and panel action hooks:

  • .unixnotis-panel-window
  • .unixnotis-panel-header-top
  • .unixnotis-panel-title-stack
  • .unixnotis-panel-title-row
  • .unixnotis-panel-search-revealer
  • .unixnotis-widget-stack
  • .unixnotis-widget-revealer
  • .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-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-has-actions
  • .unixnotis-panel-card-no-actions
  • .unixnotis-panel-card-has-body
  • .unixnotis-panel-card-has-summary

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

Placeholder row hooks:

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

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;
}

How css-check fits into theming now

noticenterctl css-check is no longer just a parser wrapper.

It now 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

It also understands more modern GTK CSS than before. Valid var(...) and calc(...) usage is no longer treated as a blanket warning just because it looks more web-like.

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