StyleKit
-
Introducing StyleKit: A unified styling API for Controls and Widgets
Qt offers two different modules for creating user interfaces: Qt Quick Controls and Qt Widgets. And traditionally you would have to choose which one to use when starting a new project.
But rather than having to choose, we want to allow Controls and Widgets to seamlessly coexist in the same application, and even side-by-side in the same window. For this to work, Controls and Widgets need to be integrated on many levels, such as event delivery, focus handling, and accessibility. But also styling. If you're mixing the two in the same window, and they are styled differently, your UI will end up fragmented and inconsistent. The same concern applies if you have a suite of applications — some written with Controls, others with Widgets — that should share a consistent look and feel.
The styling APIs for the two modules are, however, fundamentally different. Widgets use QStyle, which is an imperative C++ API. Controls use Qt Quick Templates, which is declarative QML. And no one wants to implement and maintain two different versions of the same style anyway.
With Qt 6.12 LTS, we're therefore introducing StyleKit, a QML-based styling API that lets you style both from a single source. In this post, we'll walk through some of its main features, with code snippets showing how to use each one.
Creating a Style
Let's start by looking at a minimal StyleKit style. As shown in the following snippet, at the root we have a Style item, which is the main type that holds the style definition, and inside it, we configure the visual appearance of a few controls.
// MyStyle.qml import QtQuick import Qt.labs.StyleKit Style { button { padding: 6 background { color: "darkseagreen" border.color: "green" shadow { opacity: 0.6 verticalOffset: 2 horizontalOffset: 2 color: "gray" } gradient: Gradient { GradientStop { position: 0.0 color: Qt.alpha("black", 0.0) } GradientStop { position: 1.0 color: Qt.alpha("black", 0.2) } } } } radioButton { background.visible: false indicator.foreground.color: "darkseagreen" } textField { background.border.color: "darkseagreen" text.color: "green" } }As the snippet shows, styling the controls is mostly about configuring property-value pairs, such as geometry and colors. For example, we give the button a green-ish background color, a semi-transparent gradient on top, and a drop shadow. The available delegates and properties that can be styled largely mirror the Qt Quick Controls API, so if you already know how to use Controls, you should quickly get up to speed with StyleKit.
Activating the style for a QML application is done using the attached StyleKit.style property:
// main.qml import QtQuick import Qt.labs.StyleKit ApplicationWindow { id: app width: 200 height: 250 visible: true // Assign the style to be used StyleKit.style: MyStyle { id: style} // Controls are used as normal Frame { anchors.fill: parent anchors.margins: 10 Column { spacing: 10 Button { text: "Button" } RadioButton { text: "RadioButton" } CheckBox { text: "CheckBox" } TextField { text: "TextField" } } } }Implementation-wise, Qt.labs.StyleKit is basically just a normal Qt Quick Controls style — built with Qt Quick Templates, just like the other built-in styles. What makes it special is that it configures itself entirely from a Style description, like the one exemplified above, rather than hardcoding its appearance. As such, StyleKit does not replace Qt Quick Templates, but offers a more convenient API on top that lets you focus on design over implementation. As a result, anything you could already do with Qt Quick Controls — using your own delegates, or mixing in custom Qt Quick Template-based controls — continues to work as before.
To set the same style in a Widgets application, use QStyleKitStyle. QStyleKitStyle is a subclass of QStyle that draws the widgets the classical way with a QPainter, but does so based on the description from a StyleKit Style:
// main.cpp #include <QtWidgets/QtWidgets> #include <QStyleKitStyle> using namespace Qt::StringLiterals; int main(int argc, char *argv[]) { QApplication app(argc, argv); // Assign the style to be used auto *style = new QStyleKitStyle(":/MyStyle.qml"_L1); QApplication::setStyle(style); // Widgets are used as normal QWidget window; auto *frame = new QFrame(&window); frame->setFrameShape(QFrame::StyledPanel); auto *frameLayout = new QVBoxLayout(frame); frameLayout->addWidget( new QPushButton(QWidget::tr("QPushButton"))); frameLayout->addWidget( new QRadioButton(QWidget::tr("QRadioButton"))); frameLayout->addWidget( new QCheckBox(QWidget::tr("QCheckBox"))); frameLayout->addWidget( new QLineEdit(QWidget::tr("QLineEdit"))); auto *windowLayout = new QVBoxLayout(&window); windowLayout->addWidget(frame); window.resize(200, 250); window.show(); return QApplication::exec(); }Here's what the style looks like for the Controls application (left), compared to the Widgets application (right):


From the screenshots, we can see that the button's background has a darkseagreen color, with a semi-transparent gradient on top and a drop shadow — exactly as configured in the style. Even though the snippet only configures a few of the controls, all other controls, such as the CheckBox, remain fully usable: any control type you leave unconfigured simply falls back to StyleKit's default style, which looks a bit like the classic Basic style.
This fallback happens per property too — notice that the button also has a small radius applied, even though we never set one ourselves. That's because the default style has its own background radius, which applies when we don't override it. The same goes for the RadioButton: it has a round indicator and some default padding between its elements. We could override any of these properties, but since we didn't, they simply fall back to the values set in the default style. This means you can get started with a new style quickly, and override controls and properties one by one until you get the appearance you want. Designing your style incrementally this way works because of property propagation, which we'll discuss next.
Property Propagation
In the snippet at the top, we configured the style for button, radioButton and textField. We could of course style all the other control types too, such as checkBox, slider, frame, scrollBar, and so on. And since StyleKit mirrors the type hierarchy in Qt Quick Controls, base types such as abstractButton, textInput, and even control are also available.
Whenever a property isn't set explicitly on a specific control type in the style, StyleKit searches up the type hierarchy to find the closest match. If, for example, the property is background.color and the control type is button, StyleKit first checks whether it's set directly on button, and if not, it checks abstractButton, otherwise control. If still not found, it continues in the same fashion inside the default fallback style, as mentioned earlier. In general, the search for a property value follows what we call the propagation chain, and type inheritance is just one part of this. We'll cover the rest later in this post.
Factoring out property assignments that are common to several control types into a shared base type can greatly reduce repetition, and make it easier to iterate and tweak the style as you go. In the next snippet, we move some of the assignments into the base types, and override only what differs in the specific control types. If we later want to change the border color to something other than darkseagreen, for example, we only need to change it in one place — in control — rather than in each specific control type.
Style { control { padding: 6 background.border.color: "darkseagreen" // Unset properties fall back to the default style } abstractButton { background { color: "darkseagreen" shadow { opacity: 0.6 verticalOffset: 2 horizontalOffset: 2 color: "gray" } gradient: Gradient { GradientStop { position: 0.0 color: Qt.alpha("black", 0.0) } GradientStop { position: 1.0 color: Qt.alpha("black", 0.2) } } } indicator { foreground.color: "darkseagreen" } // Unset properties fall back to control } textInput { // We want all text inputs (textField, textArea, etc.) to // have green text, so we set it here in the base type. text.color: "green" // Unset properties fall back to control } radioButton { // Hide the background to only show the indicator and label background.visible: false // Unset properties fall back to abstractButton } button { // Unset properties fall back to abstractButton // which is already styled the way we want. } textField { // Unset properties fall back to textInput // which is already styled the way we want. } }Control States and Transitions
All styling properties can have different values depending on the state of the control. To give a property a different value in a specific state, you prepend the state to the property. This is exemplified in the snippet below: the button's background color is configured to change depending on whether it's hovered, pressed, disabled, or checked. States can even be nested, as with button.checked.hovered.background.color, to apply a different hover color specifically when the button is checked.
button { background.color: "darkseagreen" hovered.background.color: "lightgreen" pressed.background.color: "palegreen" disabled.background.color: "gainsboro" focused.background.border.width: 2 checked { background.color: "lightsteelblue" hovered.background.color: "lightblue" pressed.background.color: "powderblue" } }Handling states declaratively like this removes the need to implement any state-handling logic yourself. Such logic often turns into deeply nested ternary expressions — such as control.pressed ? pressedColor : control.hovered ? hoveredColor : control.checked ? checkedColor : normalColor — that need to happen in the right order, and are therefore prone to end up different from one style to the next.
By default, a control changes appearance immediately when entering a new state. To instead use a transition, assign a standard QtQuick Transition to the transition property. Like other style properties, transition also participates in state resolution (and property propagation) and can therefore take a different value per state. This lets you define distinct transitions for specific states, or disable transitions entirely for others.
In the snippet below, the base transition slowly fades the button's background color on a state change. But since we want the button to respond instantly when pressed, we set the transition to null for that state.
button { background.color: "darkseagreen" background.border.color: "green" hovered.background.border.color: "olivedrab" hovered.background.color: "lightgreen" pressed.background.color: "darkseagreen" transition: Transition { ColorAnimation { properties: "background.color, background.border.color" easing.type: Easing.OutQuad duration: 500 } } pressed.transition: null }Here's what this looks like in the Widgets application:

Support for Multiple Themes
StyleKit has built-in support for light and dark themes through the light and dark properties on a Style. A Theme lets you define the appearance each control should have when that theme is active. Any property left unset falls back to the value set in the Style, following the same propagation chain described earlier. This allows you to factor out the property values that are common to all themes and assign them directly in the Style — typically structural information such as borders and radii — while letting each theme focus on the properties that differ — typically the colors.
Beyond light and dark, you can also define any number of additional themes using CustomTheme. While the built-in themes are applied automatically based on the operating system's color scheme, a CustomTheme must be given a name and activated explicitly by the application. Apart from this difference, all themes work the same way.
In the following snippet, we define three themes — light, dark, and HighContrast — each with its own color profile.
Style { light: Theme { applicationWindow.background.color: "#f0f0f0" control.background.color: "ghostwhite" control.background.border.color: "lightgray" control.text.color: "black" button.background.border.color: "lightslategrey" button.background.color: "lightsteelblue" button.hovered.background.color: "lightskyblue" } dark: Theme { applicationWindow.background.color: "#404040" control.background.color: "#505050" control.text.color: "#e0e0e0" button.background.border.color: "darkgray" button.background.color: "dimgray" button.hovered.background.color: "lightslategray" } CustomTheme { name: "HighContrast" theme: Theme { applicationWindow.background.color: "whitesmoke" control { background.color: "whitesmoke" background.border.color: "black" background.border.width: 2 text.color: "black" hovered.text.bold: true } } } }A Theme can also contain palette and font configurations. These are typically used for assigning static traits, such as letter spacing and font family, and contain a broad set of properties that can be adjusted. A few of the font attributes, such as italic and bold, are also available on each control's text delegate, and setting them there can be preferable, since they allow different values per control state.
Theme { palettes { // Static theme palettes system.link: "blue" system.linkVisited: "purple" } fonts { // Static theme fonts button.family: "Verdana" button.letterSpacing: 2 } // Dynamic font attributes set directly on the text delegate button.text.bold: false button.hovered.text.bold: true }Style Variations
Any non-trivial design system often defines multiple variations of the same control — a button, for example, might come in mini, normal, and large sizes. Such variations act like style hints that the application developer can optionally apply to some of the controls to make them stand out. The design system might also suggest that some variations should apply automatically whenever a control is used as a child or descendant of another control. A Switch in a ListView delegate, for example, might need a more compact design than a Switch in a ToolBar. StyleKit handles such variations through StyleVariation, which lets you define alternative styling for specific controls, or for entire sections of your UI.
A StyleVariation works much like a Theme: it has a name, and it lets you override properties that take effect when the variation is active. The propagation chain described earlier applies, and an active variation takes precedence over the current theme or style where it's defined. You can also redefine the same StyleVariation inside each theme, to give it a different configuration depending on which one is active.
The following snippet shows two style variations, mini and alert. In each, we configure how a control with that variation attached should look, fine-tuning the appearance per control type where needed. Properties left unset will fall back to be resolved through the propagation chain.
Style { button { background.color: "darkseagreen" background.border.color: "green" } StyleVariation { name: "mini" control.padding: 2 control.text.pointSize: 10 button.background.height: 20 } StyleVariation { name: "alert" control.background.color: "red" control.text.color: "white" button.background.border.width: 2 button.hovered.background.color: "darkred" button.background.border.color: "black" } }Applying the variation on a control in the application is done through StyleVariation.variations. This is an attached property that takes a list of variation names, each applied in sequence. A StyleVariation propagates, so if you attach it to an item with child controls, such as a ToolBar, it also applies to those children.
Column { spacing: 10 Button { text: "Normal" } Button { StyleVariation.variations: ["mini"] text: "Mini" } Button { StyleVariation.variations: ["alert"] text: "Alert" } Button { StyleVariation.variations: ["mini", "alert"] text: "Mini Alert" } }Here's what this looks like:

In Qt 6.12, StyleVariation only applies to QML applications. Support for Widgets is planned, but requires further research before it's ready.
Custom Controls
Even though StyleKit is meant for styling the built-in Controls and Widgets, you can still extend it with your own controls — whether implemented completely from scratch, or built on top of Qt Quick Templates. All it takes is defining a CustomControl. A CustomControl behaves exactly like the other built-in controls, except that it needs an ID (named controlType), which can be any integer you choose. It participates in the propagation chain mentioned earlier, and it can also be redefined (with the same ID) inside each Theme, to give it a different appearance depending on which one is active.
The following snippet shows an example of how we could define a style for a custom control named DraggableSpinBox, which lets you change its value by dragging the text instead of clicking up/down buttons:
Style { id: style readonly property int draggableSpinBox: 100 // unique ID CustomControl { controlType: style.draggableSpinBox background.color: "lightsteelblue" hovered.background.color: "lightblue" pressed.background.color: "skyblue" } }And the snippet below shows a brief example of how the DraggableSpinBox could be implemented by an application, so that it respects the style above. The main component here is the StyleReader. A StyleReader is an application-facing interface for reading properties from the active Style, based on the state of the control, such as whether it's hovered or pressed. It takes care of resolving the correct values based on the active Theme, any effective StyleVariations, the propagation chain, and so on.
// DraggableSpinBox.qml import QtQuick import Qt.labs.StyleKit Rectangle { id: root property real value: 0 StyleReader { id: styleReader // Use the same ID as the `CustomControl` in the `Style` controlType: StyleKit.style.draggableSpinBox // Forward the control's state, so the style properties // used to colorize the root `Rectangle` resolve correctly hovered: hoverHandler.hovered pressed: dragHandler.active } HoverHandler { id: hoverHandler } DragHandler { id: dragHandler target: null onActiveTranslationChanged: { const min = Math.min(100, activeTranslation.x * 0.5) root.value = Math.max(0, min) } } // Bind properties to the resolved values from the `Style` implicitWidth: styleReader.background.width implicitHeight: styleReader.background.height color: styleReader.background.color radius: styleReader.background.radius Text { font: styleReader.font anchors.centerIn: parent text: root.value.toFixed(0) } }Custom Delegates
StyleKit takes care of drawing the application controls. And it does so using delegates that know how to render themselves based on the properties from the Style. Still, there can be cases when the default implementation falls short of what you're trying to achieve — perhaps you want a completely custom look, or need extra items inside the delegate. To ensure that StyleKit never ends up placing a hard limit on what you can do, you can always swap out the default delegates with your own.
The following snippet shows how the handle delegate of a slider can be changed to one that draws a rotating star instead:
Style { slider { handle.color: "green" handle.delegate: Item { implicitWidth: star.implicitWidth implicitHeight: star.implicitHeight // delegateStyle is assigned by StyleKit, and gives // access to all the resolved style properties for this // delegate. Bind to it to respect configured style // properties, such as colors. required property DelegateStyle delegateStyle Text { id: star text: "★" color: parent.delegateStyle.color font.pixelSize: 34 NumberAnimation on rotation { loops: Animation.Infinite from: 0 to: 360 duration: 6000 } } } } }
For Widgets, custom delegates are currently ignored, and the built-in rendering will always apply. So if you're designing a style that targets both Controls and Widgets, and need the two to look the same, you should avoid custom delegates for now. That said, QStyleKitStyle — the QStyle subclass that draws a StyleKit style — can itself be subclassed by your application if you need to override how certain widgets or their underlying primitives are drawn with a QPainter.
Summary
In this post, we've introduced StyleKit, a new module in Qt 6.12 that unifies the styling of Qt Quick Controls and Widgets. We covered some of the main features you can expect, with brief example snippets to give you an idea of how to use them in practice.
StyleKit begins its life as a Qt Labs module. This gives us a few Qt releases to gather real-world feedback and refine the API into a final version before it graduates. We'd therefore love to hear from you — feel free to file a Jira task, assigned to either me or Doris Verria, with any suggestions or comments that can help us shape StyleKit to fit your styling needs even better. It's also worth noting that it's not entirely finished yet: not all elements can be styled, and certain features — such as drop shadows — are still missing for Widgets.
For more information about StyleKit and how it can be used to solve your styling needs, check out the examples and documentation shipped with Qt 6.12.
-
J JKSH referenced this topic
-
Hi,
Nice ! It's looking awesome !
One thing that you didn't mention is how it compares to the stylesheet QStyle. Does it also "break" the control style as it is not using the platform style ? -
This sounds great. We are still using QWidgets together with stylesheets. Sounds like this could be a better replacement for stylesheets. And if we can get a consistent look between QWidgets and QML we might even start using QML.
-
Hi,
Nice ! It's looking awesome !
One thing that you didn't mention is how it compares to the stylesheet QStyle. Does it also "break" the control style as it is not using the platform style ?@SGaist We haven't had much time yet to benchmark StyleKit against Qt Style Sheets. Most of our benchmarking so far has focused on start-up time for Qt Quick Controls, such as comparing a Fusion-like style implemented with StyleKit against the built-in Fusion style (on macOS and an i.MX 8). For our test app (with 150 checkboxes), we found that the StyleKit version takes about 250ms, while the built-in version takes about 210ms. But comparing performance against Qt Style Sheets is on our to-do list, and the results might even deserve a post of their own.
Does it also "break" the control style as it is not using the platform style ?
Not 100% sure what you mean by "control style" (the QtQuick.Controls import?), but yes, StyleKit will, by design, produce a style that looks exactly the same across all platforms. Even controls you don't explicitly style in your Style will simply use the default style, as mentioned in the post, rather than the platform's native style. If you'd rather have your app look native, you should import QtQuick.Controls, or one of the native styles directly (e.g. QtQuick.Controls.macOS).
-
@SGaist We haven't had much time yet to benchmark StyleKit against Qt Style Sheets. Most of our benchmarking so far has focused on start-up time for Qt Quick Controls, such as comparing a Fusion-like style implemented with StyleKit against the built-in Fusion style (on macOS and an i.MX 8). For our test app (with 150 checkboxes), we found that the StyleKit version takes about 250ms, while the built-in version takes about 210ms. But comparing performance against Qt Style Sheets is on our to-do list, and the results might even deserve a post of their own.
Does it also "break" the control style as it is not using the platform style ?
Not 100% sure what you mean by "control style" (the QtQuick.Controls import?), but yes, StyleKit will, by design, produce a style that looks exactly the same across all platforms. Even controls you don't explicitly style in your Style will simply use the default style, as mentioned in the post, rather than the platform's native style. If you'd rather have your app look native, you should import QtQuick.Controls, or one of the native styles directly (e.g. QtQuick.Controls.macOS).
@Richard-Moe-Gustavsen thanks for the details !
I saw the QStyle mention and thought it would be a dedicated style but I was wondering whether it would act like a proxy or be a full style. That question has been answered :-) -
Great work ! I have yet to try it (and won't do it for a serious project soon since we already have our own QQC custom style) but it seems it would ease the creation of custom style.
I'm not sure I agree with the disabled state excluding the highlighted one though. Does it mean that it won't be possible to differentiate disabled highlighted buttons from disabled non-highlighted buttons?
I use highlighted buttons for CTA (call to action) buttons, I still want a disabled one to be different from a normal button.I guess the intended way to do CTA is using variations now?
-
Great work ! I have yet to try it (and won't do it for a serious project soon since we already have our own QQC custom style) but it seems it would ease the creation of custom style.
I'm not sure I agree with the disabled state excluding the highlighted one though. Does it mean that it won't be possible to differentiate disabled highlighted buttons from disabled non-highlighted buttons?
I use highlighted buttons for CTA (call to action) buttons, I still want a disabled one to be different from a normal button.I guess the intended way to do CTA is using variations now?
@GrecKo According to the docs, a control "... can be highlighted in order to draw the user's attention towards it." But does it really make sense to draw attention to a disabled button or item delegate? We could allow it, but then it might come as a surprise that disabled controls also sometimes render as highlighted. That said, this is exactly the kind of thing we still have a chance to change while StyleKit is in Labs. If you think it's needed, feel free to file a report with a use-case, and we can take another look.
guess the intended way to do CTA is using variations now?
With StyleKit, yes!
-
According to the docs, a control "... can be highlighted in order to draw the user's attention towards it." But does it really make sense to draw attention to a disabled button or item delegate?
I'd say yes, the "Quick reply" button here in the forum is highlighted, one could easily imagine it being disabled when the message box is empty but still highlighted (with a less saturated text color or background maybe).
We could allow it, but then it might come as a surprise that disabled controls also sometimes render as highlighted.
If it is allowed and the precedence kept as is, I believe it will only render as highlighted if there's a
disabled.highlighted(or vice-versa) ControlStateStyle provided, that's quite explicit and intentional. A simpledisabledstyle will still be preferred over a simplehighlighted.If you think it's needed, feel free to file a report with a use-case
will do.
-
According to the docs, a control "... can be highlighted in order to draw the user's attention towards it." But does it really make sense to draw attention to a disabled button or item delegate?
I'd say yes, the "Quick reply" button here in the forum is highlighted, one could easily imagine it being disabled when the message box is empty but still highlighted (with a less saturated text color or background maybe).
We could allow it, but then it might come as a surprise that disabled controls also sometimes render as highlighted.
If it is allowed and the precedence kept as is, I believe it will only render as highlighted if there's a
disabled.highlighted(or vice-versa) ControlStateStyle provided, that's quite explicit and intentional. A simpledisabledstyle will still be preferred over a simplehighlighted.If you think it's needed, feel free to file a report with a use-case
will do.
An other use case that comes to mind is the table view. You have a row selected then open modal dialog: you still expect that the selection to be highlighted even if disabled. The color would be dimmed or even different but still shows the fact that something is selected.
-
An other use case that comes to mind is the table view. You have a row selected then open modal dialog: you still expect that the selection to be highlighted even if disabled. The color would be dimmed or even different but still shows the fact that something is selected.
An other use case that comes to mind is the table view. You have a row selected then open modal dialog: you still expect that the selection to be highlighted even if disabled. The color would be dimmed or even different but still shows the fact that something is selected.
Personally, I'd consider that "Inactive" (defocussed) rather than "Disabled" (suppressed -> deselected), following the Active/Inactive/Disable concept at https://doc.qt.io/qt-6/qpalette.html#ColorGroup-enum. Are you thinking of a different meaning for "Disabled", @SGaist and @GrecKo?
As for dimming, the current pattern in Qt Quick Controls is to cover the whole window with a dimming overlay (https://doc.qt.io/qt-6/qml-qtquick-controls-popup.html#dim-prop), rather than change the state of individual elements of the window
-
@SGaist We haven't had much time yet to benchmark StyleKit against Qt Style Sheets. Most of our benchmarking so far has focused on start-up time for Qt Quick Controls, such as comparing a Fusion-like style implemented with StyleKit against the built-in Fusion style (on macOS and an i.MX 8). For our test app (with 150 checkboxes), we found that the StyleKit version takes about 250ms, while the built-in version takes about 210ms. But comparing performance against Qt Style Sheets is on our to-do list, and the results might even deserve a post of their own.
Does it also "break" the control style as it is not using the platform style ?
Not 100% sure what you mean by "control style" (the QtQuick.Controls import?), but yes, StyleKit will, by design, produce a style that looks exactly the same across all platforms. Even controls you don't explicitly style in your Style will simply use the default style, as mentioned in the post, rather than the platform's native style. If you'd rather have your app look native, you should import QtQuick.Controls, or one of the native styles directly (e.g. QtQuick.Controls.macOS).
@Richard-Moe-Gustavsen said in StyleKit:
comparing performance against Qt Style Sheets is on our to-do list
Other than performance, it's also worth comparing the usability of StyleKit vs. style sheets. I remember getting quite frustrated with style sheets before and had to fight hard to make things look exactly how I want.
I've yet to do a deep dive into StyleKit, but from a quick glance it seems more intuitive+flexible and less "surprising" than style sheets.