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):
[image: ecfe29a1-e56f-4293-aa6e-41c76f512cb8.png]
[image: 11e26295-9c2f-41f9-862a-3786dfa638a0.png]
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:
[image: 39171279-6e1f-4d9b-b40b-ddcbd3dd8310.gif]
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:
[image: 5cc8473a-84ea-4f99-9bce-12b68064a8a6.png]
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
}
}
}
}
}
[image: 480b26ed-1edb-44e0-bc47-f61ce222c851.gif]
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.