Material Design theming for PySide6 using qt-material library. Theme management, runtime switching, custom colors, density scaling, and CSS overrides.
Use the qt-material library for all UI styling. It provides Material Design
themes with built-in dark/light variants, color customization, density scaling,
and runtime theme switching.
pip install qt-material
import sys
from PySide6 import QtWidgets
from qt_material import apply_stylesheet
app = QtWidgets.QApplication(sys.argv)
window = QtWidgets.QMainWindow()
# Apply dark teal theme
apply_stylesheet(app, theme='dark_teal.xml')
window.show()
app.exec()
from qt_material import list_themes
# All built-in themes
themes = list_themes()
# ['dark_amber.xml', 'dark_blue.xml', 'dark_cyan.xml',
# 'dark_lightgreen.xml', 'dark_pink.xml', 'dark_purple.xml',
# 'dark_red.xml', 'dark_teal.xml', 'dark_yellow.xml',
# 'light_amber.xml', 'light_blue.xml', 'light_cyan.xml',
# 'light_cyan_500.xml', 'light_lightgreen.xml', 'light_pink.xml',
# 'light_purple.xml', 'light_red.xml', 'light_teal.xml',
# 'light_yellow.xml']
Light themes should use invert_secondary=True for proper contrast:
apply_stylesheet(app, theme='light_teal_500.xml', invert_secondary=True)
extraThe extra dictionary customizes colors, fonts, and density:
extra = {
# Semantic button colors
'danger': '#dc3545',
'warning': '#ffc107',
'success': '#17a2b8',
# Font
'font_family': 'Segoe UI',
# Density Scale (-3 compact to +3 spacious)
'density_scale': '-1',
}
apply_stylesheet(app, theme='dark_teal.xml', extra=extra)
Use setProperty('class', ...) to apply semantic colors:
delete_btn = QPushButton('Delete')
delete_btn.setProperty('class', 'danger')
warning_btn = QPushButton('Warning')
warning_btn.setProperty('class', 'warning')
save_btn = QPushButton('Save')
save_btn.setProperty('class', 'success')
Controls padding/margin of all widgets:
| Value | Effect |
|---|---|
'-3' | Most compact |
'-2' | Compact |
'-1' | Slightly compact |
'0' | Default |
'+1' | Slightly spacious |
'+2' | Spacious |
'+3' | Most spacious |
extra['QMenu'] = {
'height': 40,
'padding': '10px 20px 10px 20px',
}
Use QtStyleTools mixin for dynamic theme changes:
from PySide6.QtWidgets import QMainWindow
from qt_material import QtStyleTools, apply_stylesheet
class MainWindow(QMainWindow, QtStyleTools):
def __init__(self) -> None:
super().__init__()
self._setup_ui()
# Apply initial theme
self._extra = {
'danger': '#dc3545',
'warning': '#ffc107',
'success': '#17a2b8',
'font_family': 'Segoe UI',
'density_scale': '-1',
}
apply_stylesheet(self, theme='dark_teal.xml', extra=self._extra)
def switch_to_dark(self) -> None:
"""Switch to dark theme."""
self.apply_stylesheet(self, 'dark_teal.xml', extra=self._extra)
def switch_to_light(self) -> None:
"""Switch to light theme."""
self.apply_stylesheet(
self, 'light_teal_500.xml',
invert_secondary=True, extra=self._extra
)
Add a built-in theme picker menu:
class MainWindow(QMainWindow, QtStyleTools):
def __init__(self) -> None:
super().__init__()
# Requires a QMenu named menuStyles in the menu bar
self.add_menu_theme(self, self.menuStyles)
class MainWindow(QMainWindow, QtStyleTools):
def __init__(self) -> None:
super().__init__()
extra = {'density_scale': '0'}
self.set_extra(extra)
self.add_menu_density(self, self.menuDensity)
Create and preview themes in real-time:
class MainWindow(QMainWindow, QtStyleTools):
def __init__(self) -> None:
super().__init__()
self.show_dock_theme(self) # Opens color picker dock
# Saved as 'my_theme.xml' in project directory
Theme colors are exposed via environment variables:
import os
from qt_material import apply_stylesheet
apply_stylesheet(app, theme='dark_teal.xml')
# Read current theme colors
primary = os.environ.get('QTMATERIAL_PRIMARYCOLOR')
primary_light = os.environ.get('QTMATERIAL_PRIMARYLIGHTCOLOR')
secondary = os.environ.get('QTMATERIAL_SECONDARYCOLOR')
secondary_light = os.environ.get('QTMATERIAL_SECONDARYLIGHTCOLOR')
secondary_dark = os.environ.get('QTMATERIAL_SECONDARYDARKCOLOR')
primary_text = os.environ.get('QTMATERIAL_PRIMARYTEXTCOLOR')
secondary_text = os.environ.get('QTMATERIAL_SECONDARYTEXTCOLOR')
theme_name = os.environ.get('QTMATERIAL_THEME')
Use these in custom widgets:
label.setStyleSheet(
f'background-color: {primary}; color: {primary_text};'
)
Extend the base theme with a CSS file:
apply_stylesheet(
app, theme='dark_teal.xml',
css_file='custom.css', extra=extra
)
Use {QTMATERIAL_*} placeholders for theme colors:
QPushButton {{
color: {QTMATERIAL_SECONDARYCOLOR};
text-transform: none;
background-color: {QTMATERIAL_PRIMARYCOLOR};
border-radius: 8px;
}}
.big_button {{
height: 64px;
font-size: 16px;
font-weight: bold;
}}
Apply to widgets:
button.setProperty('class', 'big_button')
stylesheet = app.styleSheet()
with open('custom.css') as f:
app.setStyleSheet(stylesheet + f.read().format(**os.environ))
Create a theme file using Android XML color format:
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<color name="primaryColor">#00e5ff</color>
<color name="primaryLightColor">#6effff</color>
<color name="secondaryColor">#f5f5f5</color>
<color name="secondaryLightColor">#ffffff</color>
<color name="secondaryDarkColor">#e6e6e6</color>
<color name="primaryTextColor">#000000</color>
<color name="secondaryTextColor">#000000</color>
</resources>
Apply:
apply_stylesheet(app, theme='my_theme.xml', invert_secondary=True)
Generate standalone .qss and .rcc files for distribution:
from qt_material import export_theme
extra = {
'danger': '#dc3545',
'warning': '#ffc107',
'success': '#17a2b8',
'font_family': 'Segoe UI',
'density_scale': '0',
'pyside6': True,
}
export_theme(
theme='dark_teal.xml',
qss='dark_teal.qss',
rcc='resources.rcc',
output='theme',
prefix='icon:/',
invert_secondary=False,
extra=extra,
)
Load exported theme:
from PySide6.QtCore import QDir
with open('dark_teal.qss', 'r') as f:
app.setStyleSheet(f.read())
QDir.addSearchPath('icon', 'theme')
import sys
from PySide6 import QtWidgets
from qt_material import apply_stylesheet
def main() -> None:
app = QtWidgets.QApplication(sys.argv)
extra = {
'danger': '#dc3545',
'warning': '#ffc107',
'success': '#17a2b8',
'font_family': 'Segoe UI',
'density_scale': '-1',
}
apply_stylesheet(app, theme='dark_teal.xml', extra=extra)
from ui.main_window import MainWindow
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
import os
from PySide6.QtWidgets import QApplication
from qt_material import apply_stylesheet, list_themes
class ThemeService:
"""Manage qt-material themes."""
DARK_THEME = 'dark_teal.xml'
LIGHT_THEME = 'light_teal_500.xml'
DEFAULT_EXTRA = {
'danger': '#dc3545',
'warning': '#ffc107',
'success': '#17a2b8',
'font_family': 'Segoe UI',
'density_scale': '-1',
}
def __init__(self, app: QApplication) -> None:
self._app = app
self._current_theme = self.DEFAULT_THEME
self._extra = dict(self.DEFAULT_EXTRA)
@property
def current_theme(self) -> str:
return self._current_theme
def is_dark(self) -> bool:
return self._current_theme.startswith('dark_')
def apply(self, theme: str | None = None) -> None:
"""Apply theme to application."""
theme = theme or self._current_theme
self._current_theme = theme
invert = not theme.startswith('dark_')
apply_stylesheet(
self._app, theme=theme,
invert_secondary=invert, extra=self._extra
)
def toggle_dark_light(self) -> None:
"""Toggle between dark and light variant of current color."""
color = self._current_theme.split('_', 1)[1]
if self.is_dark():
self.apply(f'light_{color}')
else:
self.apply(f'dark_{color}')
def set_density(self, scale: int) -> None:
"""Set density scale (-3 to +3)."""
self._extra['density_scale'] = str(scale)
self.apply()
def get_color(self, key: str) -> str:
"""Get current theme color from environment."""
return os.environ.get(f'QTMATERIAL_{key.upper()}', '')
@staticmethod
def available_themes() -> list[str]:
return list_themes()
card = QFrame()
card.setObjectName('card')
# qt-material styles QFrame automatically
# For extra elevation, add custom CSS
# Default — follows theme automatically
btn = QPushButton('Default Action')
# Semantic variants via class property
danger_btn = QPushButton('Delete')
danger_btn.setProperty('class', 'danger')
warning_btn = QPushButton('Archive')
warning_btn.setProperty('class', 'warning')
success_btn = QPushButton('Approve')
success_btn.setProperty('class', 'success')
Access theme colors for custom painting:
import os
class CustomWidget(QWidget):
def paintEvent(self, event) -> None:
painter = QPainter(self)
primary = QColor(os.environ.get('QTMATERIAL_PRIMARYCOLOR', '#009688'))
painter.setBrush(primary)
painter.drawRect(self.rect())
apply_stylesheet once in main.py before showing windowsextra dict for semantic colors — don't hardcode hex valuessetProperty('class', ...) for semantic button stylinginvert_secondary=True for all light themesdensity_scale at -1 or -2 for information-dense desktop appsos.environ when needed in custom widgetscss_file for project-specific overrides instead of inline stylesThemeService pattern for centralized theme management| ❌ Don't | ✅ Do Instead |
|---|---|
| Write full QSS from scratch | Use apply_stylesheet() from qt-material |
| Hardcode colors in widgets | Use os.environ['QTMATERIAL_*'] or extra dict |
Import from ui.styles.design_system | Use qt-material's apply_stylesheet + extra |
Create custom Colors, Spacing, Typography classes | Let qt-material handle visual consistency |
Use setStyleSheet() directly on widgets | Use css_file parameter or setProperty('class', ...) |
Forget invert_secondary on light themes | Always set invert_secondary=True for light themes |