Skip to content

Styling

locainin edited this page Aug 12, 2026 · 14 revisions

Styling

UnixNotis uses GTK 4.18+ CSS. Theme files live at the paths configured in config.toml and are hot-reloaded when edited. The configured files are loaded directly; an exported manifest is not needed for local styling.

GTK 4.18 custom properties are part of the supported theme contract. Use var(...) and calc(...) where they make a design easier to maintain; noticenterctl css-check validates the same active files the live UI loads.

For a design-first workflow that connects panel structure, content, palette, component styling, state testing, and preset export, start with Build Your Setup. This page is the CSS reference for that workflow.

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

Fresh installation and repair provision missing CSS files when their configured paths stay inside the config root. Existing regular files are never replaced. An external configured file is preserved when present and is left uncreated when missing; runtime then uses the embedded fallback for that layer without stopping the UI.

Edit the file that owns the component being changed. Shared palette and token changes belong in base.css; panel, popup, widget, and media rules belong in their matching files. Keeping one owner for each rule makes load order predictable and avoids a second stylesheet that silently wins over the component theme.

Compatibility notes:

  • [theme].style_css is accepted as a legacy alias for [theme].base_css.
  • UnixNotis does not automatically migrate a standalone legacy style.css; copy wanted rules into the configured base_css file.
  • noticenterctl css-check follows the paths configured in config.toml, not just the standard file names above.
  • theme.toml is legacy/theme-directory compatibility metadata, not part of normal preset export. It does not decide whether local CSS is read.

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.

Transitions and keyframe animations

UnixNotis theme files can use the transition and keyframe features accepted by the installed GTK4 CSS runtime. Motion is entirely a presentation concern: config.toml chooses which surfaces and states exist, while component CSS decides whether those states change immediately, ease between values, or animate on a loop.

Use transitions for hover, checked, active, playing, and focus feedback. Use @keyframes sparingly for an intentional ambient effect, and attach the animation to a stable UnixNotis class rather than visible text or a fragile widget position. noticenterctl css-check validates these declarations through the real CSS path.

See Build Your Setup for a small motion recipe and Glitchy Glow for a preset with active panel, card, and empty state animations.

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

GTK 4.18 CSS support

UnixNotis requires GTK 4.18 or newer and uses one CSS capability baseline.

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.

Supported CSS path

The supported runtime is GTK 4.18 or newer. Use the shared custom properties and stable UnixNotis hooks directly:

  • :root { --unixnotis-* }
  • var(--unixnotis-...)
  • calc(...) where the property accepts it
  • state hooks on panel, popup, notification, widget, and media surfaces

Direct values remain valid for one-off styling. There is no second legacy GTK runtime path to maintain, and the installer rejects older GTK versions before changing the system.

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 the generated --unixnotis-* custom-property layer. One config knob can therefore drive the shared theme contract without forcing duplicate values into every selector.

Shared custom properties

UnixNotis emits shared --unixnotis-* values that themes can consume directly.

Examples:

  • --unixnotis-ui-font-family
  • --unixnotis-monospace-font-family
  • --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 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.collapsed-group-preview
  • .unixnotis-group-row-collapsed, .unixnotis-group-row-expanded
  • .unixnotis-stack-layer, .unixnotis-stack-layer-middle, .unixnotis-stack-layer-back

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
  • .unixnotis-panel-sender-visual

Conversation avatars use .unixnotis-panel-sender-visual. The neutral .unixnotis-panel-card-thumbnail slot is shared by lead visuals, while .unixnotis-panel-content-image and .unixnotis-panel-sender-visual identify visual treatments that may add a framed decorative surface. A conversation avatar remains a plain 56×56 px identity image in the panel; it is not an optional content thumbnail.

Media cards use .unixnotis-media-art-frame for the bounded artwork surface and .empty when a player has no art. Native cover art is intentionally given a quiet frame so missing artwork does not look like a broken image.

Timed DND menu hooks:

  • .unixnotis-dnd-menu
  • .unixnotis-dnd-menu-content
  • .unixnotis-dnd-menu-title
  • .unixnotis-dnd-menu-choice
  • .unixnotis-dnd-menu-choice-indefinite
  • .unixnotis-dnd-menu-separator

Inline reply hooks:

  • .unixnotis-inline-reply
  • .unixnotis-inline-reply-entry
  • .unixnotis-inline-reply-send
  • .unixnotis-inline-reply-error

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.collapsed-group-preview
  • .unixnotis-group-row-collapsed, .unixnotis-group-row-expanded

Placeholder row hooks:

  • .unixnotis-empty
  • .unixnotis-empty-label
  • .unixnotis-stack-layer
  • .unixnotis-stack-layer-middle and .unixnotis-stack-layer-back

Notification stack notes:

  • stack layers are decoration inside the notification row, not separate notification records
  • .unixnotis-stack-layer-middle and .unixnotis-stack-layer-back identify the measured rear layers; their visibility is controlled by group depth
  • keep layer min-height, top margin, and bottom margin compact for an iOS-like card depth
  • large layer heights make collapsed groups look like empty cards instead of stacked cards

Shared state hooks:

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

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 and placement are configurable:

  • panel.empty_text controls the label used when no notifications exist.
  • panel.no_matching_text controls the label used when an active search matches none of the existing notifications.
  • panel.empty_offset_top shifts the label down when widgets are visible.
  • panel.empty_alignment selects auto, start, center, or end placement.

auto starts below visible widgets and centers when widgets are hidden. Explicit alignment does not change when widgets collapse. The top offset is applied only to start alignment.

Example:

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

Widget styling examples

The widget stack exposes .unixnotis-widget-density-comfortable and .unixnotis-widget-density-compact so a theme can refine either configured density without repeating a second configuration switch.

.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

Compound, pseudo-state, and descendant selectors are reduced to their rightmost structural UnixNotis hook when that hook is modeled. This lets selectors such as .unixnotis-media-controls-side .unixnotis-media-control-strip contribute real geometry instead of producing an unhelpful blanket complex-selector warning. Unknown or unmodeled hooks still warn.

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. Watcher creation and directory-registration failures are reported during UI startup instead of failing silently in a detached worker.

Hover performance rules

Hover effects should target the widget whose pixels change. Avoid ancestor :hover selectors that restyle a composite subtree such as a quick slider, because one pointer transition can invalidate the slider, its labels, segment children, and GTK's internal scale nodes together.

Prefer:

.unixnotis-quick-slider-volume
.unixnotis-quick-slider-scale slider:hover {
  border-color: #00a2ff;
}

.unixnotis-quick-slider-icon:hover {
  background-image: linear-gradient(
    150deg,
    alpha(@unixnotis-accent, 0.20),
    alpha(@unixnotis-accent-2, 0.18)
  );
}

Avoid rules such as .unixnotis-quick-slider-volume:hover slider or .unixnotis-quick-slider-volume:hover .unixnotis-quick-slider-value. Keep scrollbar geometry, padding, margins, widths, heights, and transforms stable across hover and active states. A color or border-color change is preferable to a hover-time layout change. Validate the final file with noticenterctl css-check.

CSS-check directives

An intentional cascade override may use this narrow source-range directive:

/* unixnotis-css-check allow-duplicate-selectors:start */

/* deliberate override */

/* unixnotis-css-check allow-duplicate-selectors:end */

Only duplicate-selector diagnostics in that range are suppressed. The checker still parses the stylesheet, validates var(...) and supported calc(...) expressions, checks imports and paths, and reports duplicate properties or other errors. Keep the range short and closed.

Popup and inline-reply hooks

Popup styling is owned by popup.css, including .unixnotis-popup-card, identity rows, close controls, action rows, and reply fields. Inline reply is shown only for an advertised KDE-compatible reply action and is authorized by the daemon; CSS cannot enable it for a notification that did not advertise it.

SVG artwork boundary

Local SVG artwork is rasterized through the bounded unixnotis-svg-renderer helper. Themes can style the resulting image, but CSS does not grant access to remote URLs or secondary SVG images. Keep artwork local and validate presets before sharing them.

Hot reload boundary

Saving base.css, panel.css, popup.css, widgets.css, or media.css triggers a debounced reload of that layer without changing config.toml or theme.toml. Missing, empty, invalid, oversized, unsafe, or unreadable files use that layer's embedded fallback. CSS reload cannot rebuild a GTK widget hierarchy; restart the affected UI after changing Rust layout or interaction code.

Clone this wiki locally