-
Notifications
You must be signed in to change notification settings - Fork 1
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-checkunderstands 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.
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 abovewidgets.css
If a file is missing, a default template is created on startup.
Panel UI:
base.csspanel.csswidgets.cssmedia.css
Popup UI:
base.csspopup.css
Later files override earlier rules.
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/3unixnotis-notification-bg-1/2
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:
- 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
- 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
- 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-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.
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.55Those 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.
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.
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.
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 togglekindis 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
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-1is the first depth layer and appears when a group has at least two notifications -
.unixnotis-stack-ghost-2is 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);
}The empty-state text is configurable in two ways:
-
panel.empty_textcontrols the label text. -
panel.empty_offset_topshifts 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;
}.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;
}noticenterctl css-check validates theme syntax, theme wiring, and common layout risks.
It helps with three different theme-authoring problems:
- Syntax and parser failures
- broken GTK CSS
- Theme wiring problems
- missing active files
- duplicate theme slots
- outside-root theme asset issues
- 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.
CSS files are watched for changes and reloaded on write. Large edits are coalesced into a single reload to avoid flicker.