Material You, Gruvbox, Catppuccin
Problem
Users want their favorite color scheme. Manually maintaining a version of your shell for Material You, Gruvbox, Catppuccin (Mocha, Latte), and others means duplicating every .qml file per scheme.
Naive Approach
Copy your entire shell into shell-materiayou/, shell-gruvbox/, shell-catppuccin/. Each copy diverges over time. Maintenance nightmare.
MentalModel
A design token system maps semantic roles to values. Swapping themes means swapping the token values — not the UI code. Your Colors.qml becomes a palette that changes at startup (or runtime) based on user preference. The UI layer never knows which scheme is active.
Idea
Create one Colors.qml per scheme: ColorsMaterialYou.qml, ColorsGruvbox.qml, ColorsCatppuccin.qml. Use a ThemeManager singleton that imports the correct one based on a config file or environment variable.
Build It
// ColorsCatppuccinMocha.qml
pragma Singleton
import Quickshell
QtObject {
readonly property color background: "#1e1e2e"
readonly property color surface: "#313244"
readonly property color text: "#cdd6f4"
readonly property color textMuted: "#6c7086"
readonly property color accent: "#89b4fa"
readonly property color border: "#45475a"
readonly property color error: "#f38ba8"
readonly property color success: "#a6e3a1"
}// ThemeManager.qml
import QtQuick
import QtQuick.Controls
import "."
QtObject {
property string currentTheme: "catppuccin-mocha"
readonly property QtObject colors: {
if (currentTheme === "catppuccin-mocha") return CatppuccinMocha
if (currentTheme === "catppuccin-latte") return CatppuccinLatte
if (currentTheme === "gruvbox-dark") return GruvboxDark
if (currentTheme === "material-you") return MaterialYou
return CatppuccinMocha
}
}// Usage — bind to ThemeManager.colors
import QtQuick
import "."
Rectangle {
color: ThemeManager.colors.surface
Text {
text: "Themed text"
color: ThemeManager.colors.text
}
}BuildIt Breakdown
Each scheme file is a pragma Singleton with identical property names but different values. ThemeManager uses JavaScript object lookup ({ ... }) as a reactive switch — when currentTheme changes, the colors binding re-evaluates and all bound UIs update automatically. No import paths change; every file binds to ThemeManager.colors.*.
Improve It
Read currentTheme from a JSON config file using FileReader. Add a theme preview panel. Support hybrid schemes (Catppuccin surfaces with Gruvbox accent). Generate schemes from a wallpaper color extractor (like Material You's monet engine).
CommonMistake
Using dynamic import to switch theme files. Dynamic imports in QML are asynchronous and break property bindings. The ThemeManager approach with a JS switch is synchronous and fully reactive.
Under the Hood
QML singletons with identical property names share the same C++ meta-object structure. When ThemeManager.colors re-evaluates, QML's binding engine invalidates all dependent bindings in a single pass. No memory is leaked because all singleton references are compile-time resolved. The JS { } object is not a QML binding — it is a getter function that re-runs when currentTheme changes.
Exercises
Add a Catppuccin Latte (light) variant to your existing dark theme.
Build a theme switcher UI with radio buttons that changes currentTheme at runtime.
Implement a color extraction algorithm that generates a theme from a user-provided wallpaper image.