-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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
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_cssis accepted as a legacy alias for[theme].base_css. - UnixNotis does not automatically migrate a standalone legacy
style.css; copy wanted rules into the configuredbase_cssfile. -
noticenterctl css-checkfollows the paths configured inconfig.toml, not just the standard file names above. -
theme.tomlis legacy/theme-directory compatibility metadata, not part of normal preset export. It does not decide whether local CSS is read.
Panel UI:
base.csspanel.csswidgets.cssmedia.css
Popup UI:
base.csspopup.css
Later files override earlier rules.
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.
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 requires GTK 4.18 or newer and uses one CSS capability baseline.
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.
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.
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 the generated --unixnotis-* custom-property layer. One config knob can
therefore drive the shared theme contract without forcing duplicate values into every selector.
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.
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.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 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.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-middleand.unixnotis-stack-layer-back
Notification stack notes:
- stack layers are decoration inside the notification row, not separate notification records
-
.unixnotis-stack-layer-middleand.unixnotis-stack-layer-backidentify 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);
}The empty-state text and placement are configurable:
-
panel.empty_textcontrols the label used when no notifications exist. -
panel.no_matching_textcontrols the label used when an active search matches none of the existing notifications. -
panel.empty_offset_topshifts the label down when widgets are visible. -
panel.empty_alignmentselectsauto,start,center, orendplacement.
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;
}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;
}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
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.
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 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.
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 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.
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.
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.