Skip to content

ui::Theme

Header: winui/ui/themes/Theme.hpp.

Everything a theme defines: the palette and accent shades, corner radii, padding and border width, the surfaces, text styles and type ramp, the focus visual, motion, popup behavior, window frame settings, and the three button sets custom controls draw with (draw_control_chrome).

A default-constructed Theme is not usable: button_normal, accent_button_* and the other button visuals have no meaningful default. Always get a Theme from Theme::light() or Theme::fluent(palette), also for a class member's default (Theme theme_ = Theme::light();, never Theme theme_{};).

A Window owns the active theme (Window::pin_theme()) and hands it to every element's measure and render. An element with a requested theme draws its subtree with with_dark_mode. The fields are plain data, so an app may take a resolved theme and change single values (metrics, type, motion) before applying it. The role colors are the exception: colors() is read-only, and a theme with other colors comes from fluent(const ThemeColors&) or classic(const ThemeColors&, ...). How each built-in control draws itself in an era (its toggle, slider, field and row-selection looks) is internal to the theme and not public. A copy of a theme carries it along unchanged.

Example:

// A dark Fluent theme with a wider focus ring, applied to a window:
ui::Theme theme           = ui::Theme::fluent(ui::Palette::fluent_dark());
theme.focus_ring_outer_width = 3.0f;
window.pin_theme(std::move(theme));

variant

ThemeVariant variant = ThemeVariant::Fluent2

The design language the theme draws in, the one its factory built it for. Under high contrast it is the variant whose contrast look this is, so check high_contrast too. A custom control reads it to ask "is this Fluent?" before reading palette.

Example:

const bool fluent = (theme.variant == ui::ThemeVariant::Fluent2 || theme.variant == ui::ThemeVariant::Fluent) &&
                    !theme.high_contrast;
const ui::Color fill = fluent ? theme.palette.card_background_default : theme.colors().field;

high_contrast

bool high_contrast = false

Whether the theme is a high-contrast one, from a system contrast theme or ThemeMode::HighContrast. Its colors are then the contrast scheme's, and palette is left default even under the Fluent variants.

dark

bool dark = false

Whether the theme draws light content on a dark background: the dark mode of a variant that has both (Fluent 2, Fluent, Mdl2, Metro), or a dark contrast scheme. Every theme sets it, and the fixed-palette eras are light. A custom control reads it to pick a per-mode value, such as a shadow's alpha or a band color, under any variant. palette.dark is set only by Theme::fluent().

palette

Palette palette

The Fluent palette the theme was derived from. Only Theme::fluent() sets it, so it is filled for the Fluent variants outside high contrast. Every other theme leaves it default, so check variant before reading it.

accent_ramp

AccentRamp accent_ramp {}

The accent shades the theme was resolved from, the Windows defaults when the theme was not resolved from the system's settings. Controls and apps that need a lighter or darker shade of the accent read it here.

with_dark_mode

std::function<Theme(bool dark)> with_dark_mode {}

The same theme resolved light (false) or dark (true), which an element with a requested theme (Element::set_requested_theme()) draws its subtree with. Set by resolve_theme() for the variants that have both. Empty for the fixed-palette eras and under high contrast, where a requested theme is ignored. It resolves the request again, so later edits to this theme are not in its result. An element with a requested theme carries the edits that are not colors (metrics, type, motion) over on top of it.

corner_radius

float corner_radius = 4.0f

Control corner radius in DIPs: buttons, fields, toggles.

overlay_corner_radius

float overlay_corner_radius = 8.0f

Popup corner radius in DIPs: flyouts, menus, dialogs.

padding

Thickness padding = Edges{.left = 11.0f, .top = 5.0f, .right = 11.0f, .bottom = 6.0f}

Button content padding.

border_width

float border_width = 1.0f

Default control border width; see ButtonVisual::border_width.

sizing

ControlSizing sizing {}

Control density overrides; all unset (standard density) by default.

focus_ring_outer_width

float focus_ring_outer_width = 2.0f

outer ring stroke, in DIPs

The two-ring focus visual (render/FocusRing.hpp's draw_focus_ring()): a 2px outer ring in colors.focus with a 1px colors.focus_inner ring right inside it. The pair fills the focus_ring_margin pixels outside the control's bounds. focus_ring_margin is how far out the outer ring's outer edge lands, half a stroke beyond its center line.

focus_ring_inner_width

float focus_ring_inner_width = 1.0f

inner ring stroke, in DIPs

focus_ring_margin

float focus_ring_margin = 3.0f

how far the two rings reach outside the control

spelling_error

Color spelling_error = Color::from_argb(0xFFC42B1C)

TextBox's misspelling squiggle. Defaults to the Fluent light theme's critical red, the red every Windows spell checker uses, which the era themes keep. Theme::fluent() takes the palette's semantic_fill_critical, a lighter red in dark themes. High contrast uses the window text color, the only one sure to show on the field background.

disabled_text_effect

TextEffect disabled_text_effect {}

How a disabled control's text is drawn, on top of colors.text_disabled: the text beside a CheckBox, RadioButton or ToggleSwitch, and a disabled TextControl. Windows 95 through 2000 draw disabled text the way they draw a disabled push button's label, engraved: a highlight copy offset by a pixel under a shadow copy. So Theme::classic() sets the same Emboss it gives button_disabled.

Text only. A disabled check mark or radio dot stays a flat gray glyph, so the indicators read their own text and never this. Windows 3.x draws disabled text in plain gray, as every non-classic theme does.

severity

Severities severity {}

InfoBar, InfoBadge and ProgressBar severity colors.

anim_faster

Duration anim_faster {83.0}

The hover and press highlight fade of every control that can be toggled.

anim_fast

Duration anim_fast {167.0}

For offset and scale moves: ToggleSwitch's knob slide, RadioButton's dot resize, and Slider's inner-circle grow/shrink into Normal/Disabled.

anim_normal

Duration anim_normal {250.0}

Slider's inner thumb circle growing into Hover/Pressed. The duration is keyed by the state being entered, so leaving Hover runs at anim_fast.

anim_fast_out_slow_in

CubicBezier anim_fast_out_slow_in = easing::ControlFastOutSlowIn

The easing curve for every offset/scale motion above, at either duration.

animations_enabled

bool animations_enabled = true

Whether controls animate at all, resolved from ThemeRequest::animations and the system setting. When false, resolve_theme() also zeroes the durations above and popup_motion, and Window ticks its tree with animations_enabled() false, so every animation the framework plays lands on its end state at once.

text_style

TextStyle text_style {}

The theme's base UI font: button labels, most control text, and menus unless chrome_text_style is set. TextControl reads type instead.

chrome_text_style

std::optional<TextStyle> chrome_text_style {}

The font menus and menu bars draw in, for an era whose chrome font differs from its content font. nullopt uses text_style. Windows 3.x sets it: its chrome is System 10pt bold, and its dialogs MS Sans Serif 8pt.

type

TypeRamp type {}

The type ramp TextControl resolves its role() against.

page

PageMetrics page {}

Page silhouette metrics.

rasterization

SetRasterization rasterization {}

How the era's shapes and text are rasterized. Window::render() applies it first, every frame. The default is antialiased shapes, grayscale text and no pixel snapping. Only the classic, Luna and Aero themes set something else.

focus_visual

FocusVisual focus_visual = FocusVisual::FluentRings

How keyboard focus is shown: the Fluent two rings (the default), the classic eras' black dotted rectangle inside the control (Windows 95 through 7, drawn in colors.focus), or Metro's dashed pair.

divider_visual

ThemeDividerVisual divider_visual = ThemeDividerVisual::EtchedGroove

Which divider geometry separators draw; EtchedGroove unless Theme::fluent().

disclosure_visual

ThemeDisclosureVisual disclosure_visual = ThemeDisclosureVisual::IconChevron

Which expand, collapse and drop-down marks controls draw; ClassicArrow in Theme::classic().

ThemePopupShadow popup_shadow = ThemePopupShadow::FluentTwoLight

The shadow an open popup casts.

PopupMotion popup_motion {}

The era's popup open/close animations.

overlay_material

std::optional<AcrylicBrush> overlay_material {}

The material that flyouts, drop-down lists, menus and the other popups that close on an outside click fill with: in-app acrylic, which blurs what is drawn behind it. Only Fluent 2 sets it (Palette::acrylic_in_app_default()), and only while the system allows transparency (OsThemeSnapshot::transparency_enabled) and no contrast theme is on. Unset, those surfaces fill with their solid color, surfaces.menu. fill_overlay_surface() makes that choice.

icon_font_family

FontName icon_font_family = "Segoe Fluent Icons"

The symbol font a control's glyphs are drawn from, passed as the font_family of render/IconGlyph.hpp's draw_icon. Windows 10 Fluent uses Segoe MDL2 Assets, and Windows 11 Segoe Fluent Icons, a redesign with the same code points. Each theme names its font.

button_corner_radius

OptionalFloat button_corner_radius {}

Button's own corner radius, for an era whose push button is rounded differently from its other controls. Unset, the default, follows corner_radius. Windows 3.x sets it, since its push button is the era's one rounded control.

surfaces

Surfaces surfaces {}

The era-specific surface colors every theme fills (see Surfaces): field borders, menus, toolbars, tabs, tooltips, link states and scroll bars.

chrome

ChromeHints chrome {}

Settings for the system-drawn window frame (see ChromeHints). The defaults leave the system's corner rounding, title bar mode and frame colors alone.

button_normal

ButtonVisual button_normal

Neutral Button (the default ButtonVariant), the look of every Button unless it opts into ButtonVariant::Accent/Subtle.

button_hover

ButtonVisual button_hover

button_pressed

ButtonVisual button_pressed

button_disabled

ButtonVisual button_disabled

accent_button_normal

ButtonVisual accent_button_normal

ButtonVariant::Accent: a filled accent surface, at most one per view.

accent_button_hover

ButtonVisual accent_button_hover

accent_button_pressed

ButtonVisual accent_button_pressed

accent_button_disabled

ButtonVisual accent_button_disabled

subtle_button_normal

ButtonVisual subtle_button_normal

ButtonVariant::Subtle: borderless, with only a wash on hover and press. Toolbar, command bar and menu buttons use it.

subtle_button_hover

ButtonVisual subtle_button_hover

subtle_button_pressed

ButtonVisual subtle_button_pressed

subtle_button_disabled

ButtonVisual subtle_button_disabled

colors

const ThemeColors& colors() const noexcept

The colors by role, filled for every era and mode (see ThemeColors), and what a custom control reads first. Read-only. A different set of colors is a different theme, made by fluent(const ThemeColors&) or classic(const ThemeColors&, ...).

with_content_colors

Theme with_content_colors(Color text, std::optional<Color> background = std::nullopt) const

A copy of this theme for drawing a container's content. Content in the default text color draws in text, and content that reads the background, such as a control that blends into its surface, sees background when one is given. A container whose surface differs from the page, such as a card, a popup or an accent-filled box, draws its children with it.

Example:

// An accent-filled box whose text follows the accent:
class AccentBoxControl : public ui::Element
{
protected:
    void render_override(ui::Renderer& renderer, const ui::RenderContext& context) const override
    {
        const ui::Theme& theme = context.theme;
        renderer.fill_rounded_rect(bounds(), 0.0f, theme.colors().accent);
        child_->render(renderer, context.with_theme(theme.with_content_colors(theme.colors().text_on_accent)));
    }

private:
    std::unique_ptr<ui::Element> child_;
};

generation

std::uint64_t generation() const

A number unique to this Theme value, never 0. Each theme built, copied or moved into gets a new one, so a cache of measurements or derived themes can tell a new or edited theme from the one it came from. Editing a field in place keeps the number. Hand the edited theme over as a new value (Window::pin_theme()) for caches to see the change.

const TextStyle& menu_text_style() const

chrome_text_style if this era has one, text_style otherwise.

resolved_button_corner_radius

float resolved_button_corner_radius() const

button_corner_radius if set, corner_radius otherwise.

for_state

const ButtonVisual& for_state(ButtonState s) const

The neutral Button's visual (button_*) for an interaction state.

subtle_for_state

const ButtonVisual& subtle_for_state(ButtonState s) const

The borderless subtle_button_* visual for an interaction state. Menu bar, command bar and toolbar buttons use it, the menu bar's items included. A Button the app places itself uses for_state() unless it opts into ButtonVariant::Subtle.

focus_ring_style

FocusRingStyle focus_ring_style() const

The focus visual settings that render/FocusRing.hpp's draw_focus_ring() and draw_focus_ring_rings() take, built from this theme's fields.

fluent

static Theme fluent(const Palette& palette)

A Fluent theme derived from palette: every value above and the built-in controls' own looks.

fluent

static Theme fluent(const ThemeColors& roles)

A Fluent theme derived from a full set of role colors. The roles that differ from the Fluent light colors, or the dark ones when roles.background is dark, replace the palette colors they come from, and everything else follows from them as in fluent(const Palette&). Change accent alone and the selection, the accent button's hover and pressed fills and the informational severity follow it. colors() of the result reads back what was passed for every role that was changed.

Example:

ui::ThemeColors colors = ui::Theme::fluent(ui::Palette::fluent_dark()).colors();
colors.accent          = ui::Color::from_argb(0xFF8764B8);
window.pin_theme(ui::Theme::fluent(colors));

classic

static Theme classic(const ClassicPalette& palette, bool hot_track, const char* font_family, bool win3x_edge = false)

A classic (Windows 95, 98, 2000) theme derived from palette, the classic counterpart of fluent(). Every classic theme, a custom scheme included, is one ClassicPalette plus this function, with no hand-assembled ButtonVisual per state. hot_track lights a button's face under the pointer, as Windows 98 does and 95 does not. font_family is the UI font, at 11 px. Name a TrueType font: bitmap fonts can't be loaded, so MS Sans Serif or System quietly gives Segoe UI. The Windows 9x and 3.x themes pass Microsoft Sans Serif and then set it at 8 pt on a 13 px line. win3x_edge switches every edge to the Windows 3.x construction in place of the 9x four-ring bevel. Windows 3.x has only face, shadow and highlight colors for buttons, but draws a four-color, uneven edge: a 1px black frame, a 1px white highlight on the top and left and a 2px gray shadow on the right and bottom (render/Bevel.hpp's draw_win3x_border). It also rounds the push button, flattens the check box to a black and white outline, marks it with an X instead of a tick, and drops the engraved disabled label.

classic

static Theme classic(const ThemeColors& roles, bool hot_track, const char* font_family, bool win3x_edge = false)

A classic theme derived from a full set of role colors, the classic counterpart of fluent(const ThemeColors&). The roles that differ from the Windows Standard scheme's colors replace the scheme color they come from: background and control_fill replace the face, and with it the bevel, text the face text, and field and field_text the window colors. selection, selection_text, text_disabled, link and overlay (the menu) replace their own. The other roles follow from those, as in classic(const ClassicPalette&, ...).

Example:

ui::ThemeColors colors = ui::Theme::classic(ui::ClassicPalette::windows_standard(), true, "Tahoma").colors();
colors.selection       = ui::Color::from_rgb(0x008080);
window.pin_theme(ui::Theme::classic(colors, true, "Tahoma"));

metro

static Theme metro(const MetroPalette& palette, bool dark)

A Metro (Windows 8.1) theme derived from palette, like fluent() and classic(). Metro has a dark and a light mode and a user accent, and dark says which mode the theme is. Pass MetroPalette::metro_dark() or metro_light() with the matching dark.

mdl2

static Theme mdl2(const Mdl2Palette& palette, bool dark, const Color& accent)

An MDL2 (Windows 10, 1507 to 1703) theme derived from palette, like fluent(), classic() and metro(). Like Metro, MDL2 has a dark and a light mode, and dark says which, together with the matching Mdl2Palette::mdl2_dark() or mdl2_light(). accent is the system's current accent, which this era uses on the controls.

early

static Theme early(const EarlyPalette& palette)

An early-Windows (1.x and 2.x) theme derived from palette, like fluent(), classic(), metro() and mdl2(), and the oldest of the five.

This era has no sense of depth and no button face, selection or disabled-text color, so its colors are an EarlyPalette. Its whole drawing model is six rules: a 1px frame, a flat fill, reverse video, black-and-white glyphs, fixed-pitch text without antialiasing, and a double 1px frame for a dialog.

light

static Theme light()

Windows 11 Fluent, light: fluent(Palette::fluent_light()). The default theme a Window starts with, and the one to reach for in member defaults.

default_light

static const Theme& default_light()

One light() theme that lives as long as the program, the default argument where a control is measured or drawn on its own. Some containers keep a pointer to the theme they were last measured with, so a default made of a temporary would dangle.

struct ControlSizing

Control density: the sizes compact density overrides. Each is nullopt in a standard theme, which leaves the control at its own standard size. ControlSizing::compact() holds the compact values, and ui/containers/DensityScope.hpp's DensityScope applies them to a subtree.

text_control_min_height

OptionalFloat text_control_min_height {}

Text field minimum height (standard: 32).

text_control_padding

std::optional<Thickness> text_control_padding {}

Text field padding (standard: the field's padding).

combo_box_min_height

OptionalFloat combo_box_min_height {}

ComboBox minimum height.

combo_box_padding

std::optional<Thickness> combo_box_padding {}

ComboBox padding (standard: its padding).

compact

static ControlSizing compact()

Compact density: text field minimum height 24 and padding 2,2,6,1, ComboBox minimum height 24 and padding 12,1,0,3.

struct SeverityVisual

Severity colors: InfoBar's icon disc and body, and ProgressBar's paused and error indicator. Read these instead of palette, which only the Fluent themes fill.

The defaults are the Fluent light colors, so a theme that sets none still draws readable bars. Theme::fluent() fills them from the palette, resolve_high_contrast() from the system's contrast colors, and an era theme sets its own where the era has them.

Field Type Default
fill Color the strong mark: InfoBar's icon disc, ProgressBar's indicator
background Color the surface behind the whole bar

struct Severities

The four InfoBar severities plus the shared neutral fill, glyph and stroke colors.

Field Type Default
informational SeverityVisual {Color::from_argb(0xFF0078D4), Color::from_argb(0x80F6F6F6)} Drawn in the accent itself. Theme::fluent() fills it from accent_button_normal.
success SeverityVisual {Color::from_argb(0xFF0F7B0F), Color::from_argb(0xFFDFF6DD)} The success, caution and critical severities.
caution SeverityVisual {Color::from_argb(0xFF9D5D00), Color::from_argb(0xFFFFF4CE)}
critical SeverityVisual {Color::from_argb(0xFFC42B1C), Color::from_argb(0xFFFDE7E9)}
neutral Color Color::from_argb(0xFF8A8A8A) The gray of InfoBadge's Informational styles. A fill only, since no InfoBar severity is neutral.
text_on_fill Color colors::White The glyph drawn on one of the fill discs, and the bar's own 1px outline.
stroke Color Color::from_argb(0x0F000000)

struct TypeRamp

The type ramp TextControl::set_role() resolves against. Every theme fills body from text_style. Metro, MDL2 and the Fluent themes give the other roles their own size and weight.

TextStyle header

One style per TextRole. A theme that sets only body leaves the rest at defaults.

subheader

TextStyle subheader

title

TextStyle title

subtitle

TextStyle subtitle

body

TextStyle body

caption

TextStyle caption

body_strong

std::optional<TextStyle> body_strong {}

BodyStrong's own style, where an era has one, as Fluent 2 does. Unset, the role is body in SemiBold (TextControl::resolved_style()).

color

RoleColors color {}

Per-role foregrounds; all unset outside Aero.

for_role

const TextStyle& for_role(TextRole role) const

The style for a TextRole (BodyStrong falls back to body; see body_strong).

color_for_role

const std::optional<Color>& color_for_role(TextRole role) const

The color for a TextRole (BodyStrong shares body's); unset means colors.text.

struct RoleColors

Per-role foreground, for an era whose type ramp carries color as well as size and weight. Aero sets it: its main instruction is #003399 Segoe UI 12pt and links are #0066CC. Every entry is unset for every other theme, so a role paints in ThemeColors::text.

Field Type Default
header std::optional<Color> One optional color per role; unset paints in ThemeColors::text.
subheader std::optional<Color>
title std::optional<Color>
subtitle std::optional<Color>
body std::optional<Color>
caption std::optional<Color>

struct PageMetrics

Page layout metrics: the grid step, the page inset and header, and the item spacing. Metro sets its 20px grid and 120/140/50 page layout here; the defaults suit every other theme.

Field Type Default
grid_unit float 4.0f the layout grid step, in DIPs
content_margin Thickness {12.0f} page content inset, in DIPs
header_height float 0.0f 0 == no header band
item_spacing float 4.0f gap between stacked items, in DIPs
command_bar_height float 48.0f a command bar's height, in DIPs

struct PopupMotion

How menus and drop-down lists open and close.

Field Type Default
menu ThemePopupAnimation ThemePopupAnimation::Slide how a menu opens
list ThemePopupAnimation ThemePopupAnimation::None how a drop-down list opens
open Duration {250.0} the opening animation's length
close Duration {83.0} a menu's fade-out; 0 closes at once