Skip to content

Window Effects

Velle Sinclair edited this page Jul 27, 2026 · 4 revisions

Window Effects

Glass, blur, drop shadows, rounded corners and the CRT post-process pass. All of it is synuirc configuration, and the shipped default is conservative: as of pkgrel 183 a fresh install boots with window effects off and the SYNAPSE wallpaper on, because effects on a machine that cannot afford them is a bad first impression that looks like a broken system.

A default change is invisible on an existing machine. Your synuirc is yours; new defaults only reach the ISO and fresh installs.

Rendering is scenefx 0.5 on wlroots 0.20. scenefx is a fork of wlroots' scene graph, which matters — see the HDR note below.


The Super+E panel

You do not have to edit a config file to tune any of this. Super+E opens a live panel with two pages, and every change applies as you make it.

Tab switches pages (Shift+Tab goes back). The status line always tells you which page you are on and what Tab will do next:

Page Holds
CRT filters The post-process pass — scanlines, curvature, aberration, glitch, phosphor, bloom
Window effects Corners, shadow, blur, halo and translucency — the 12 rows below

Same keys on both pages:

Key Does
/ — or k / j Move between rows
/ — or h / l Adjust the selected row
Space Toggle (see below — it means different things per page)
s Save the current page
Esc · q · Return Close

The panel is modal: while it is open it swallows unmodified keys, so stray letters cannot leak into the window underneath. Super+… combos still reach the global bind table, so the rest of the desktop keeps working.

Space is page-sensitive

  • On the CRT page, Space is the master on/off for the whole effects pass, from any row — you never have to navigate to the top row to kill it.
  • On the window page, Space toggles the switch governing the row you are on. On any shadow row it toggles Drop shadow; on any blur row (including Glass halo) it toggles Backdrop blur; on Window opacity it toggles Translucency. So pressing it on a greyed-out Shadow size row lights the shadow, rather than doing nothing because that row is not itself a switch.
  • Rounded corners has no governing switch — 0 px is off. Space there says so rather than silently ignoring you.

Saving is per page

s saves the page you are on, and the two pages persist to separate filesfilters.state for CRT, uifx.state for window effects. That is deliberate: a shared file would mean saving a shadow you had just tuned also wrote back whatever the CRT page happened to be holding.

Row values are stored under their synuirc key names, so a value you tune in the panel can be pasted straight into a config file — the panel and synuirc are one vocabulary. The exception is the translucency pair, which lives in theme.state because the theme owns it; storing a second copy would give startup two answers.

The window-effects rows

Row Range Step synuirc key
Rounded corners 0–40 px 2 corner_radius
Drop shadow on/off shadow
Shadow size 0–80 px 2 shadow_blur_sigma
Shadow spread 0–64 px 1 shadow_spread
Shadow drop −32–32 px 1 shadow_offset_y
Shadow opacity 0–100% 5% shadow_opacity
Backdrop blur on/off blur
Blur radius 1–20 1 blur_radius
Blur passes 1–5 1 blur_passes
Glass halo 0–40 px 2 glass_halo
Translucency on/off transparency
Window opacity 50–100% 2% active_opacity

Window opacity floors at 50% on purpose: a window that can go fully invisible is a window you cannot find again.


The knobs

All of these go in ~/.config/synui/synuirc (or /etc/synui/synuirc), and are the same values the panel above writes.

Transparency and glass

Key Default Range What it does
transparency off on/off Master switch for translucent windows
active_opacity 0–1 Focused window opacity
inactive_opacity 0–1 Unfocused window opacity
foot_alpha 0–1 foot's alpha, decoupled from the slider above
corner_radius 12 0–48 Rounded window corners

Blur

Key Default Range What it does
blur on on/off Backdrop blur behind translucent windows
blur_passes 3 1–5 Dual-kawase passes
blur_radius 5 1–20 Per-pass radius
blur_noise 0.02 0–1 Dither, to break up banding
blur_brightness 0.90 Post-blur brightness
blur_contrast 1.00 Post-blur contrast
blur_saturation 1.15 Post-blur saturation
glass_halo 0 0–64 px Let the blur reach past the window as a ring

Shadows

Key Default Range What it does
shadow on on/off Drop shadow
shadow_blur_sigma 18 0–80 Gaussian sigma
shadow_spread 0 0–64 px Solid ring outside the window before the tail
shadow_offset_x / _y 0 / 6 px Offset
shadow_opacity 0.45 0–1
shadow_color black
clip_csd_margin on on/off Crop clients to their declared xdg geometry

CRT filters (Super+E)

Key Default What it does
effects off Master switch for the post-process pass
effect_scanline 0.35
effect_curvature 0.25
effect_aberration 0.40 Chromatic aberration
effect_glitch 0.60
effect_phosphor off Monochrome tint: amber / green / white
effect_mono 0.90 How strongly the phosphor tint desaturates
effect_bloom 0.55 Phosphor glow

Persisted to filters.state, adjustable live with a slider per effect — this is the CRT page of the Super+E panel, and Space on any row is the master switch for the whole pass.

Night light

night_light / night_light_temp, or Super+Shift+B.


Reading a shadow: whose is it?

This is the single most useful skill for debugging "the shadow looks wrong on this app", and it took a measurement pass to establish.

synui's shadow is a gaussian. No hard edge, reach = sigma + spread, and shadow_offset_y makes the top band differ from the sides. The shader clips at 2·σ_eff, where coverage is about 2% — never a visible step.

A hard cliff back to the wallpaper is a client margin. GTK crops its own shadow to a rectangular margin that differs per side, so the tell is a hard edge at an asymmetric distance — tens of pixels on one side, a handful on another.

So an app whose shadow looks "overgrown and square-cornered" next to everything else is drawing its own. It was never synui's shadow.

Why Firefox is different

Firefox never binds xdg-decoration. synui declares SERVER_SIDE, but a client only obeys if it binds the protocol, so Firefox keeps its GTK frame and an invisible CSD shadow margin outside the window geometry it declares — and paints a heavy shadow into it, on top of synui's own.

Proven by logging geometry against wlr_surface_get_extents:

firefox   geo = 26,23  896x490   inside   ext 948x542
foot      geo == ext exactly

Measured, that margin is a ~36 px near-black slab at ~85% darkening with a hard step, against a server-decorated app's 12 px soft ring.

clip_csd_margin (default on) clips the client tree to wlr_xdg_surface.geometry, so every window's ring is the one synui draws — same width, same rounded corners. It also takes the margin's input region off the grab ring, which is the documented cause of "Firefox can't be resized by its edges".

Three things that are easy to get wrong there:

  1. Clip the client tree, not view->scene_tree. wlr_scene_subsurface_tree_set_clip() recurses into every subsurface tree below the node it is given, and xdg popups are parented beside the client tree — so clipping the parent crops the menus. synui captures view->client_tree right after wlr_scene_xdg_surface_create(), when the tree has exactly one child.
  2. Skip the no-op case. A clip that covers everything is not free: it puts a source box and dest size on the scene buffer, and that alone shifted the blurred backdrop around a terminal by ~7/255. Guarded on extents ⊄ geometry, so every SSD client keeps its exact pixels.
  3. wlr_box_equal is NULL/empty-safe, so set_clip(NULL) on an unclipped tree early-returns — cheap enough for the per-commit path.

shadow_spread: the 50% ceiling

box_shadow.frag insets the solid rectangle by sigma, so only the gaussian tail lands outside the window. Peak darkening at the border is therefore about half of shadow_opacity50% is a hard ceiling even at opacity 1.0, against Firefox's measured ~85%.

shadow_spread lifts that ceiling without patching scenefx: grow the node by sigma + S instead of sigma, and the shader's own inset now lands S px outside the window. Full alpha for S px, then the tail. Node corners grow with it; a square window stays square; S = 0 is byte-identical to the old geometry, which is why it is the default.

Measured: the 12 px band outside a window's border goes from 60.0 to 46.1 luminance at shadow_spread = 8.

A maximized or fullscreen window has no shadow, and the Win95 theme's SYN_CHROME_BEVEL chrome returns a shadow of 0. Both are deliberate.


glass_halo: letting the blur past the window

glass_halo = 14 puts a blurred ring outside the window — the backdrop blur extends past the frame instead of stopping at it.

The history is instructive, because the first two attempts both looked like config problems and were not.

pkgrel 181 — it only affected app-native-glass windows. The blur companion node was built only for a translucent view, and transparency defaults to off. So on a desktop with no transparency line in its config, the only window with a blur node at all was Firefox, and no value of glass_halo could give an ordinary app a ring.

If your config has no transparency line, most window-effect settings will appear to do nothing on most windows. Turn transparency on first.

pkgrel 182 then widened the gate to (translucent || halo > 0), so server-decorated apps got the ring for the first time.

pkgrel 184 — the ring had a square top corner. The cause was the per-buffer construction. A decorated window is two stacked buffers, and their corner radii carry the seam rule: the titlebar rounds the top, the content rounds the bottom, and the join between them is straight. Growing those boxes outward inherits the seam — so the content buffer's ring covered the whole left edge with square top corners, and the titlebar contributed no ring at all. The seam rule is right inside the window and wrong outside it.

view_halo_update() now owns it: one wlr_scene_blur node on view->frame, grown glass_halo px on every side, with corner_radii_all(radius + halo) and the window's own rounded rect cut out via clipped_region so it is a pure ring. Lowered under the shadow, with the same maximize/fullscreen suppression the shadow has.

Two things fell out of moving it to the frame: the per-buffer path got its transparency mask back, and an opaque window no longer runs a full-window blur pass just to show a 14 px ring.

The halo is drawn above the shadow, so it blurs the spread into a smooth ramp rather than compounding it. glass_halo = 14 alone actually brightens the band (60 → 80.7) by covering the shadow; with shadow_spread = 8 it lands back at 61.6, but smooth.


Firefox glass

synui-firefox-glass toggles it. Firefox is treated as app-native glass: it needs browser.tabs.allow_transparent_browser (FF 128+), which the helper sets.

Two rules:

  • off must revert the prefs. Leaving them set means Firefox stays translucent with no glass behind it.
  • Page content is opaque by design. Only the chrome goes glass; a transparent web page would be unreadable.

KDE blur

synui implements org_kde_kwin_blur, so KDE apps can go glass at all — but note that Dolphin never binds it. If you are testing KDE blur, confirm with WAYLAND_DEBUG that the client actually binds the interface before concluding anything about the compositor.


HDR and 10-bit

The display panel tells an HDR monitor from a merely 10-bit one by reading the EDID, not by asking the plane. Every 10-bit plane used to be labelled "HDR-ready", which is wrong: plenty of 10-bit panels are not HDR at all.

Real HDR10 output is blocked on scenefx, and scenefx is a fork of wlroots' scene graph — so the work is not "wait for wlroots", it is "port or patch the fork". wp_color_management_v1 is exported as of the 0.20 port.

Two things the night-light work established, which apply to anything touching output colour:

  • Night light goes on the output state. Setting it anywhere else is ignored while window effects are on.
  • A NULL gamma LUT must be committed, or it never turns off. The witness is modetest's GAMMA_LUT property, not the compositor's own log.

Verifying an effect actually changed something

Effects are easy to talk yourself into seeing. Two things make a comparison honest:

  • Change one knob and restart the compositor between arms. Reloading config in place leaves other state moving, so a whole-image comparison tells you nothing.
  • Compare a measurement, not an impression — a column of pixels walking outward from the window border, rather than two screenshots side by side.

The headless rig used for this, and its traps, are in Development Notes.


See also: The Desktop, synui, Troubleshooting.

Clone this wiki locally