The app launched, the tray icon showed up, the window opened, and every label in it was dark gray on a dark gray background. Nothing crashed and nothing printed an error. The text just wasn't readable.
I ran into this with KTailctl, the Kirigami-based Tailscale GUI, installed as a Flatpak on a Fedora 43 GNOME 50 Wayland session. That combination makes it worse than a normal cosmetic bug. When a VPN client's window comes up blank, your first instinct is that the VPN is broken, not the color palette. I spent the first few minutes checking tailscale status before I noticed I could select the "invisible" text and it showed up highlighted.
That's the tell, by the way. If you drag-select across an empty-looking area and words appear, you don't have a rendering bug or a dead backend. You have a theme mismatch.
What I Expected
On a GNOME desktop with dark mode enabled, a Qt app should do one of two things:
- Follow the system preference and render dark, with light text.
- Ignore the preference and render in its default light theme, with dark text.
Either is fine. Both give you readable text. What I didn't expect was a third option where the app picks half of each: the background from one source and the foreground colors from another.
Flatpak's pitch is that the app brings its own runtime and doesn't care what the host has installed. For libraries, that's great. For theming, it means the app is sitting in a sandbox trying to guess what the desktop outside looks like, and it doesn't always guess consistently.
What Actually Happened
The short version: a KDE app on GNOME inside a Flatpak has two separate places it can get colors from, and they disagreed.
Here's the longer version. A Kirigami/QML app like KTailctl doesn't render with a single palette. Roughly, it has:
-
The Qt palette (
QPalette), which comes from the Qt platform theme plugin. On GNOME, withQT_QPA_PLATFORMTHEMEunset, Qt seesXDG_CURRENT_DESKTOP=GNOMEand loads its built-in GNOME theme. Recent Qt 6 releases read the dark/light preference through the XDG desktop portal (org.freedesktop.appearance→color-scheme), so this side says "dark." -
The KDE color scheme (
KColorScheme), which Kirigami'sorg.kde.desktopstyle uses for a lot of its text and control colors. That comes fromkdeglobals. Inside the sandbox, the app's config lives under~/.var/app/<app-id>/config/, and on a GNOME machine there's usually nokdeglobalsthere at all. No file means KDE's defaults, and the defaults are Breeze light, which means dark text.
Put those together and you get a dark window background from the Qt palette with dark text from the KDE color scheme. Invisible.
On a Plasma desktop this never happens. Plasma ships the kde platform theme plugin and a real kdeglobals, so both sources agree because they're the same source. On GNOME outside a sandbox, distro packages often patch around it or pull in integration plugins. Inside a Flatpak you get neither. The host's GTK settings don't apply, the host's ~/.config/kdeglobals (if you even have one) isn't visible, and the only bridge to the host preference is the portal, which only one of the two color sources reads.
Which side "wins" depends on the exact runtime version. I saw it on org.kde.Platform 6.10. It isn't a new class of bug. Qt 5-era Flatpaks had the same problem, and the old fix was the QGnomePlatform theme extension, which has since been archived upstream. Qt 6 is supposed to handle GNOME natively, and for plain Qt Widgets apps it mostly does. Kirigami apps that also read KDE color schemes are where it falls apart.
Libadwaita doesn't help. GNOME's move to Libadwaita means there's less of a GTK theme for anything to imitate, and the color-scheme portal key is now the only reliable signal of what the user wants. If an app's theming stack doesn't fully consume that one key, it ends up with a split palette.
Diagnosing It Before Changing Anything
Before you start throwing environment variables at it, confirm what you're dealing with. First, which runtime is the app on:
# Which runtime and version the app is built against
flatpak info --show-runtime org.fkoehler.KTailctl
# org.kde.Platform/x86_64/6.10
# Any overrides already applied (Flatseal writes here too)
flatpak override --user --show org.fkoehler.KTailctl
Next, check what the host is actually advertising through the portal. This is what the sandboxed app sees, not what GNOME Settings shows you:
# 0 = no preference, 1 = prefer dark, 2 = prefer light
gdbus call --session \
--dest org.freedesktop.portal.Desktop \
--object-path /org/freedesktop/portal/desktop \
--method org.freedesktop.portal.Settings.ReadOne \
org.freedesktop.appearance color-scheme
If that returns <uint32 1>, the app is being told to go dark. Now look at which platform theme plugins the runtime actually ships, because you can only select ones that exist inside the sandbox:
# List platform theme plugins visible inside the sandbox
flatpak run --command=sh org.fkoehler.KTailctl -c \
'find / -path "*platformthemes*" -name "*.so" 2>/dev/null'
Then check whether the app has a KDE config of its own:
ls -la ~/.var/app/org.fkoehler.KTailctl/config/
# no kdeglobals here = KColorScheme falls back to Breeze light
Finally, you can make Qt tell you which platform theme it actually loaded. It's noisy, but grep makes it usable:
flatpak run --env=QT_DEBUG_PLUGINS=1 org.fkoehler.KTailctl 2>&1 \
| grep -i platformtheme
Dark mode from the portal, no kdeglobals, and the GNOME platform theme loaded: that's the split-palette setup described above.
The Fix
Whatever you do, the goal is to give the app one source of truth for colors. You can make both sides agree on dark, or make both sides agree on light. What you can't do is leave them reading from different places.
Test every option with a one-off flatpak run --env=... first. It doesn't persist anything, so a bad guess costs you nothing.
Option 1: Use the KDE platform theme and give it a color scheme
This is the fix I prefer for Kirigami apps. It makes the Qt palette and KColorScheme both come from kdeglobals, so they can't disagree.
First, test it without persisting anything:
flatpak run --env=QT_QPA_PLATFORMTHEME=kde org.fkoehler.KTailctl
If the runtime includes the kde platform theme plugin (the find above will tell you), the app should now render consistently. On GNOME that will probably be consistently light, since there's still no kdeglobals. Light and readable is already a win. If you want dark, give the app a dark scheme. The KDE runtime usually ships the Breeze color scheme files, and their format is compatible with kdeglobals:
APP=org.fkoehler.KTailctl
mkdir -p ~/.var/app/$APP/config
# Copy Breeze Dark from inside the runtime into the app's private kdeglobals
flatpak run --command=cat $APP /usr/share/color-schemes/BreezeDark.colors \
> ~/.var/app/$APP/config/kdeglobals
If that path doesn't exist in your runtime version, run ls /usr/share/color-schemes/ inside the sandbox to see what's there. If you have Plasma color schemes installed on the host, you can also copy one over from there. Once it renders the way you want, persist the environment variable:
flatpak override --user --env=QT_QPA_PLATFORMTHEME=kde org.fkoehler.KTailctl
The tradeoff is that the app stops following GNOME's dark/light toggle. It uses whatever scheme you put in its kdeglobals. For a tray utility I open a few times a day, I'm fine with that. If you switch modes on a schedule, keep reading.
Option 2: Force a single Qt Quick style
If the app is Kirigami/QML, the split usually comes from the org.kde.desktop style pulling colors from KColorScheme. Forcing a style that only reads the Qt palette removes the second source:
# Test first
flatpak run --env=QT_QUICK_CONTROLS_STYLE=Fusion org.fkoehler.KTailctl
# Persist if it looks right
flatpak override --user --env=QT_QUICK_CONTROLS_STYLE=Fusion org.fkoehler.KTailctl
Recent Qt 6 releases make Fusion generate a dark palette when the platform reports a dark color scheme, so this keeps the portal-driven dark/light behavior. The downside is cosmetic: it won't look like Breeze. Some Kirigami components also look a bit off outside their native style. Readable and slightly plain beats native-looking and blank.
Option 3: Force the GTK platform theme
The research floating around for this issue often suggests QT_QPA_PLATFORMTHEME=gtk. For Qt 6 the plugin is named gtk3, not gtk, and it only works if the runtime actually ships it. Most of the time the find command above won't list it in the KDE runtime, and Qt quietly falls back to the default when you name a plugin that doesn't exist. That's one more silent failure on top of the first one. If your runtime does include it:
flatpak run --env=QT_QPA_PLATFORMTHEME=gtk3 org.fkoehler.KTailctl
Check with QT_DEBUG_PLUGINS=1 that it actually loaded before you trust it.
Quick fix vs. permanent fix
| Approach | Persists | Scope | Undo |
|---|---|---|---|
flatpak run --env=VAR=value <app> |
No | That one launch | Close the app |
flatpak override --user --env=... |
Yes | Your user, that app | flatpak override --user --reset <app> |
| Flatseal "Environment" section | Yes | Same as above (writes the same override file) | Delete the entry in Flatseal |
flatpak override --user --env=... with no app ID |
Yes | Every Flatpak for your user | flatpak override --user --reset |
Don't use the last row casually. Setting QT_QPA_PLATFORMTHEME=kde globally will also apply to Qt apps on runtimes that don't ship that plugin, and you'll be debugging a different app next week. Scope overrides to the app ID.
To undo everything and get back to a clean state:
flatpak override --user --reset org.fkoehler.KTailctl
rm ~/.var/app/org.fkoehler.KTailctl/config/kdeglobals
Why This Matters
When you'll hit this, specifically: a Qt or KDE app (especially a Kirigami/QML one) installed from Flathub, running on GNOME or another non-Plasma desktop, with dark mode on. Wayland makes it more likely, because the old X11-era hacks of reading X resources or the host's GTK settings files are even further out of reach. Every runtime bump is a chance for the behavior to change, so an app that rendered fine last month can turn invisible after a flatpak update with no change on your side.
The part that stays with me isn't the fix. It's how much time a failure like this takes when nothing tells you it's a failure. A crash gives you a stack trace. A blank window that responds to clicks gives you nothing, and it pushes you toward the wrong subsystem. With a VPN client, "the UI shows nothing" sounds a lot like "it isn't connected," so you end up debugging networking that works fine. I wrote about the same pattern in a very different context in the blog pipeline fence corruption post: output that looks plausible and is quietly wrong is worse than output that fails loudly, because none of your error detection fires.
The sandbox angle is the other lesson. Flatpak isolates the app from host libraries, which is the point. It also isolates the app from host configuration, which people forget. A theme file, a /tmp path, a GPU driver: each one is something the app assumes about its environment, and the sandbox can quietly invalidate that assumption. I hit the /tmp version of this with Claude running headless in a sandbox, and the driver version shows up with the NVIDIA container toolkit. Different layers, same shape: the host and the container each have a version of the truth, and the bug lives in the gap between them.
What I'd do differently, and what I'd tell anyone running Qt Flatpaks on GNOME:
-
Select-all first.
Ctrl+Aor a drag-select on a blank-looking window tells you in two seconds whether the content is there. Do it before you touch the network stack. -
Check the portal, not the settings panel. The
gdbusReadOnecall shows you what the sandbox actually receives. GNOME Settings shows you what GNOME thinks. -
Pick one color source on purpose. Either the KDE platform theme plus an app-local
kdeglobals, or a Qt Quick style that only reads the Qt palette. Mixing them is the bug. -
Verify the plugin exists before you name it.
QT_QPA_PLATFORMTHEMEpointing at a missing plugin fails silently.findinside the sandbox plusQT_DEBUG_PLUGINS=1takes thirty seconds and saves you from stacking a second silent failure on the first. - Scope overrides per app. A global override works until it hits a runtime that doesn't match, and then you're back to staring at a blank window wondering what changed.
None of this is in the Flatpak docs or the app's README, because technically nothing is broken. Each component is doing what it was designed to do. The bug only shows up when you put them together.
Top comments (0)