pyside6-modern-widgets
Cross-platform desktop widgets for PySide6. The package provides frameless window chrome, navigation, and tabs while retaining familiar Qt widget APIs.
ModernWindow: a frameless replacement for top-levelQWidgetwindows with selectedQMainWindow-compatible methods.ModernDialog: a framelessQDialogthat preserves the standard dialog API.ModernMessageBox: aQMessageBoxsubclass with themed frameless chrome, native buttons, keyboard handling, and convenience methods.ModernMenu: a nativeQMenuwith Windows 11 system acrylic (and an opaque fallback elsewhere) plus rounded outer and selected-item backgrounds.ModernMenuBar: aQMenuBarthat createsModernMenudrop-down menus.NavigationSidebar: a collapsible navigation sidebar.NavigationView: a sidebar and synchronized page stack in one widget.TabView: a WinUI-inspired tab widget.
Supported environment
Supports Windows, macOS, and Linux with Python 3.10-3.12, PySide6 6.8.3, and the Fusion style. Window backgrounds, including the custom title bar, use the same Qt-painted, wallpaper-colored theme behavior on every platform.
On Windows, the custom chrome retains native activation, moving, resizing, minimize/maximize/restore transitions, Aero Snap, shadows, and the system menu. Windows 11 additionally provides DWM-rounded corners and Snap Layouts from the custom maximize button; Windows 10 uses an opaque square-corner surface. On platforms without equivalent frameless-window APIs, Qt supplies system moving, resizing, and a menu with the available window commands.
Installation
pip install pyside6-modern-widgets
Upgrading to 0.5.0
Version 0.5.0 adds modern dialogs, message boxes, menus, and menu bars, plus independent title-bar text/icon visibility and centered title text. It also improves Windows maximize/restore behavior and content-aware navigation layout. See the changelog for the full release notes.
When upgrading from 0.4.x, remove uses of WatercolorStyle,
theme_with_watercolor_style, ORIGINAL_LIGHT_THEME, and ORIGINAL_DARK_THEME.
These exports and the title-bar Theme Style submenu have been removed. Widgets
now follow desktop-wallpaper colors by default; LIGHT_THEME, DARK_THEME,
ModernTheme, and widget-level setTheme() remain available for local overrides.
Automatic navigation overlay thresholds now depend on the current page's
minimum width instead of fixed window widths. Use setAutoSidebarOverlay(False)
and setSidebarOverlay() if your application needs explicit control.
PyInstaller
The installed package automatically registers its PyInstaller hook. Applications
using these widgets can be frozen normally without package-specific
--hidden-import or --add-data options:
pyinstaller your_app.py
Example
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QApplication, QLabel
from pyside6_modern_widgets import ModernWindow
app = QApplication([])
window = ModernWindow()
window.setWindowTitle("Modern window")
file_menu = window.menuBar().addMenu("&File")
exit_action = QAction("Exit", window)
exit_action.triggered.connect(window.close)
file_menu.addAction(exit_action)
window.setCentralWidget(QLabel("Hello"))
window.resize(800, 500)
window.show()
app.exec()
ModernWindow.menuBar() returns a ModernMenuBar, so menus created from a
title or icon automatically use ModernMenu, including nested submenus.
ModernMenuBar itself provides a transparent background and rounded selection
highlight, including when constructed manually. It follows its containing
modern window's theme, or the global theme when used on its own.
When space is limited, its overflow button also opens a ModernMenu with the
same rounded surface and selection styling as the regular drop-down menus.
To place a manually created menu bar in the title bar's left control area:
from pyside6_modern_widgets import ModernMenuBar
menu_bar = ModernMenuBar(window)
menu_bar.setNativeMenuBar(False)
menu_bar.addMenu("&File").addAction("Open")
window.titleBar.addCustomWidget(menu_bar, align="left")
window.setTitleVisible(False)
setTitleVisible() controls only title text. setIconVisible() independently
controls the title bar icon. Both default to True and preserve the actual
window title and icon used by the operating system. Updating either while it is
hidden does not show it again. isTitleVisible() and isIconVisible() return
the configured visibility, even when the window itself is hidden. An empty
window icon is not drawn, regardless of the icon visibility setting.
The icon stays at the far left whenever it is visible. Title text is left-aligned
by default, between the icon and left custom widgets such as menus. Use
setTitleAlignment("center") to center only the text on the window, with menus
following the icon, or setTitleAlignment("left") to restore the default order.
titleAlignment() returns the selected mode. In narrow windows, the centered
text stays within the space between the left controls and the window buttons.
Blank space remains available for dragging. Right custom widgets appear before
the window buttons. All six methods are also available on window.titleBar.
An existing top-level QWidget subclass can keep its direct layout when its
base class changes to ModernWindow. The standard QWidget(parent, f)
constructor shape and window flags are preserved:
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel, QVBoxLayout
from pyside6_modern_widgets import ModernWindow
class ToolWindow(ModernWindow):
def __init__(self, parent=None):
super().__init__(parent, Qt.WindowType.Tool)
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Tool content"))
Use either a layout installed directly on ModernWindow or its optional
menuBar(), addToolBar(), statusBar(), and setCentralWidget() compatibility
APIs. The two layout models intentionally cannot be mixed in one window.
ModernDialog accepts ordinary Qt layouts directly and retains exec(),
accept(), reject(), and the standard dialog result codes:
from PySide6.QtWidgets import QDialogButtonBox, QLabel, QVBoxLayout
from pyside6_modern_widgets import ModernDialog
dialog = ModernDialog(window)
dialog.setWindowTitle("Settings")
layout = QVBoxLayout(dialog)
layout.addWidget(QLabel("Dialog content"))
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.exec()
ModernMessageBox provides the common information, question, warning, and
critical flows while returning QMessageBox-compatible standard buttons:
from pyside6_modern_widgets import ModernMessageBox
answer = ModernMessageBox.question(
window,
"Confirm",
"Continue with this operation?",
ModernMessageBox.StandardButton.Yes | ModernMessageBox.StandardButton.No,
ModernMessageBox.StandardButton.No,
)
Message text, details, checkboxes, button ownership, default and escape buttons,
return values, and completion signals are handled by QMessageBox itself.
The component customizes its palette, background, and title bar. It uses Qt's
widget message box on every platform so this appearance remains available.
ModernMenu accepts the same common constructor forms as QMenu and works
with ordinary QAction instances, separators, checkable actions, and submenus:
from PySide6.QtGui import QAction
from pyside6_modern_widgets import ModernMenu
menu = ModernMenu("Actions", window)
menu.addAction(QAction("Open", menu))
menu.addSeparator()
menu.addMenu("Recent")
Themes
The process-wide manager defaults to System mode with wallpaper colors enabled.
Select a mode after creating QApplication, before creating windows. Use the Fusion
style for consistent palette support across platforms; the library does not change
the application's style. Existing widgets update when the theme changes.
from PySide6.QtWidgets import QApplication
from pyside6_modern_widgets import ModernWindow, ThemeMode, theme_manager
app = QApplication([])
app.setStyle("Fusion")
manager = theme_manager()
manager.setMode(ThemeMode.SYSTEM) # Or ThemeMode.LIGHT / ThemeMode.DARK.
manager.setWallpaperEnabled(False) # Optional: use the exact base theme colors.
window = ModernWindow()
window.show()
app.exec()
| API | Contract |
|---|---|
setMode(ThemeMode.SYSTEM / LIGHT / DARK) |
Select the user's preference. Accepts a ThemeMode enum. |
mode() |
Return the selected preference, even when System currently resolves to Dark. |
isDark() |
Return whether the resolved mode is Dark. |
theme() |
Return the effective immutable ModernTheme tokens. |
setThemes(light=..., dark=...) |
Replace the two base themes without changing mode or wallpaper policy. |
setWallpaperEnabled(bool) / wallpaperEnabled() |
Control wallpaper accents independently of mode. |
refreshWallpaperTheme() |
Request an asynchronous refresh; no effect while wallpaper is disabled. |
modeChanged(mode) |
Emitted only when the selected preference changes. |
themeChanged(theme) |
Emitted only when effective tokens change, after the application palette is applied. |
wallpaperEnabledChanged(enabled) |
Emitted when the wallpaper policy changes. |
System mode reads QApplication.styleHints().colorScheme() and listens for
colorSchemeChanged; it never infers the OS setting from the palette it has
written. An unknown system scheme falls back to Light. Fixed Light/Dark modes
ignore system changes visually but remember them for the next switch to System.
An OS appearance change can emit themeChanged while mode() remains SYSTEM.
Repeatedly setting the same effective state does not emit duplicate signals.
Use the manager on the QApplication GUI thread. Configuration before application
creation is supported; it attaches on the first subsequent theme() or setter
call (including when a globally themed widget is constructed). The library owns
the application's semantic palette roles once attached. It does not install a
global style sheet or store preferences automatically.
Custom colors and wallpaper
Customize a pair of themes using semantic tokens, not per-widget color literals:
from dataclasses import replace
from pyside6_modern_widgets import DARK_THEME, LIGHT_THEME, ThemeMode, theme_manager
manager = theme_manager()
manager.setWallpaperEnabled(False)
manager.setThemes(
light=replace(LIGHT_THEME, accent="#0067C0", on_accent="#FFFFFF", focus="#0067C0"),
dark=replace(DARK_THEME, accent="#60CDFF", on_accent="#003047", focus="#60CDFF"),
)
manager.setMode(ThemeMode.SYSTEM)
Supply a light-colored theme in light and a dark-colored theme in dark;
theme names are labels and do not control mode selection. accent and
on_accent default to None: Qt's native Accent, Highlight, HighlightedText, and
Link roles remain inherited, so the system accent is preserved across light/dark
switches. Explicit strings override the selection background/text pair; restore
None to resume inheritance. To read the currently resolved system accent, use
QApplication.palette().color(QPalette.ColorRole.Accent) (or Highlight for
selection backgrounds). Wallpaper colors do not override these roles. surface_alternate,
tooltip_surface, and link_visited cover additional Qt palette roles.
Enabling wallpaper colors allows the manager to derive focus, watercolor_base,
and watercolor_spots from the wallpaper, leaving other base tokens intact.
Disabling immediately restores the selected base theme, stops monitoring, and
discards pending results. Re-enabling can use cached colors while requesting a
fresh sample. If no wallpaper is available, the configured base theme is used.
Sampling always resolves against the latest mode and base themes.
Wallpaper discovery, metadata checks, and sampling run off the GUI thread. A file watcher and a one-second metadata poll detect changes; only changed images are resampled. Repeated refresh requests are coalesced. Transient query errors retain the current colors until a later successful refresh.
Inactive windows use a solid background (#F3F3F3 for light themes, the surface
color for dark themes). Activation restores the wallpaper effect with a reversible
250 ms linear fade. Layout metrics remain independently configurable through
ModernMetrics.
Title text and menu bars use a 50%-alpha foreground in Qt's Inactive palette
group. Qt selects the group automatically; activation does not rewrite styles
or change layout. Title-bar window and button icons read the same palette group
when painted, preserving their original colors and icon modes. Button backgrounds,
hover/pressed behavior, and disabled rendering remain handled by Qt's style.
Ordinary page content retains its normal inactive contrast.
Windows 11 menu acrylic follows the owner theme, including changes while a menu is open. Very light native acrylic tints are limited to lightness 240 so a pure white theme surface does not wash out the backdrop. This leaves the Qt surface palette and opaque fallback unchanged.
Local overrides and application pages
ModernWindow, ModernDialog, ModernMessageBox, NavigationView,
NavigationSidebar, and TabView share this contract:
window.setTheme(DARK_THEME) # Fix this component and its internal chrome.
window.setTheme(None) # Resume following the global manager.
The constructor's theme= argument has the same semantics. Overrides stay fixed
across global mode and wallpaper changes. Setting a window theme does not
recursively override independently themed library widgets placed inside it.
ModernMenu inherits its owner's Qt palette (including submenus); ModernMenuBar
uses the nearest themed ancestor, or the global theme when standalone.
Ordinary Qt controls inherit the application or parent palette, including disabled
text, placeholders, selection colors, tooltips, links, and alternating surfaces.
Explicit widget palettes and hardcoded QSS colors can override that inheritance.
The public palette_for_theme(theme, base=None) helper creates a matching palette
without changing application state. For custom QSS or painting, read theme()
initially and subscribe to themeChanged:
from PySide6.QtWidgets import QLabel
from pyside6_modern_widgets import theme_manager
class StatusLabel(QLabel):
def __init__(self, parent=None):
super().__init__("Ready", parent)
self.applyTheme(theme_manager().theme())
theme_manager().themeChanged.connect(self.applyTheme)
def applyTheme(self, theme):
self.setStyleSheet(f"color: {theme.text_muted};")
Monochrome library icons are recolored by their components. Application-owned icons and custom artwork should be refreshed by the application when needed.
Saving the user's preference
Persist the selected mode, not the currently resolved light/dark appearance:
from PySide6.QtCore import QSettings
from pyside6_modern_widgets import ThemeMode, theme_manager
settings = QSettings("Example", "MyApp")
manager = theme_manager()
saved_mode = settings.value("appearance/mode", "system", type=str)
try:
mode = ThemeMode(saved_mode)
except ValueError:
mode = ThemeMode.SYSTEM
manager.setMode(mode)
manager.modeChanged.connect(lambda mode: settings.setValue("appearance/mode", mode.value))
The previous manager-level setTheme(), setFollowsSystemTheme(), and
followsSystemTheme() APIs are removed. Use setMode() and setThemes();
widget-level setTheme() remains the local-override API.
TabView uses the standard Qt argument order: addTab(widget, text) or
addTab(widget, icon, text). The former reverse (widget, text, icon) order is
not supported.
The runnable navigation example includes interactive window, dialog, and message
box pages. A separate multi-tab example is also available in the
examples directory.
NavigationView automatically uses an overlay sidebar when expanding it beside
the current page would compress the page below its minimum width. A small
hysteresis margin prevents repeated mode changes near that width. On return to
the side-by-side layout, it restores the expand/collapse intent last selected
with the sidebar toggle. Overlay mode starts with its sidebar collapsed.
Applications with a custom responsive policy can call
setAutoSidebarOverlay(False) and control the mode with setSidebarOverlay().
ModernWindow intentionally remains based on QWidget, so it is suitable for
top-level primary and auxiliary windows. Its compatibility surface is limited to the common
menuBar(), addToolBar(), statusBar(), and setCentralWidget() methods; it does
not implement QMainWindow docking or state-management features.
The bundled window and navigation icons are provided by Icons8 and remain subject to the Icons8 license.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyside6_modern_widgets-0.5.5.tar.gz.
File metadata
- Download URL: pyside6_modern_widgets-0.5.5.tar.gz
- Upload date:
- Size: 108.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8907862e184ce1d151f85c7fdfca66ecd2b22eec209574aa6f2ddf18ad4f205f
|
|
| MD5 |
3ecbd8f9f8da9ff367ddb52fdadd4619
|
|
| BLAKE2b-256 |
f82b816935b446f8d38c1c69d191b1560221cbe05ed424b5762d426ac00603df
|
Provenance
The following attestation bundles were made for pyside6_modern_widgets-0.5.5.tar.gz:
Publisher:
publish.yml on zero-ljz/pyside6-modern-widgets
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyside6_modern_widgets-0.5.5.tar.gz -
Subject digest:
8907862e184ce1d151f85c7fdfca66ecd2b22eec209574aa6f2ddf18ad4f205f - Sigstore transparency entry: 2818610154
- Sigstore integration time:
-
Permalink:
zero-ljz/pyside6-modern-widgets@40df9fd4f921476a60f21fa088686b6c4ba4c57a -
Branch / Tag:
refs/tags/v0.5.5 - Owner: https://github.com/zero-ljz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@40df9fd4f921476a60f21fa088686b6c4ba4c57a -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyside6_modern_widgets-0.5.5-py3-none-any.whl.
File metadata
- Download URL: pyside6_modern_widgets-0.5.5-py3-none-any.whl
- Upload date:
- Size: 89.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0fb112c219359cc92bb5b2fe4d2c5ed23629b75202258697e92d3e616d28e8e7
|
|
| MD5 |
9c21453fb66a15e29665e8ac2803f29c
|
|
| BLAKE2b-256 |
3f9c02d5733198c0df7f621e4aacfbb92ced573c5c81c9098202a201c5006e86
|
Provenance
The following attestation bundles were made for pyside6_modern_widgets-0.5.5-py3-none-any.whl:
Publisher:
publish.yml on zero-ljz/pyside6-modern-widgets
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyside6_modern_widgets-0.5.5-py3-none-any.whl -
Subject digest:
0fb112c219359cc92bb5b2fe4d2c5ed23629b75202258697e92d3e616d28e8e7 - Sigstore transparency entry: 2818610174
- Sigstore integration time:
-
Permalink:
zero-ljz/pyside6-modern-widgets@40df9fd4f921476a60f21fa088686b6c4ba4c57a -
Branch / Tag:
refs/tags/v0.5.5 - Owner: https://github.com/zero-ljz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@40df9fd4f921476a60f21fa088686b6c4ba4c57a -
Trigger Event:
release
-
Statement type: