NativeDesktop

Overrides

Every way to change how a native default looks or behaves, from one element's style up to the whole app, and when each is the right one.

The default for every component is the OS's own, so start with no overrides and add one only for a concrete difference you want. Each layer below changes less than the one after it: pick the smallest that does the job. The Overrides example uses every layer in one window.

Example: Overrides. Every override layer in one window: element style, per platform, native mapping, theme (app, window, subtree) and patterns. Source in examples/overrides. It also runs live in the docs app.
Overrides on macOS

Captured on macOS 27.0, 2026-10-14.

You wantLayerReach for
One element tinted, rounded or in another fontelement style.bg(), .rounded(), .font()
One element different on one OSper platform.on_macos(..), .on_linux(..)
A different native control, or a drawn onemapping.native(Mapping::..), .drawn()
New colours, accent or radiithemeApplication::theme, window.set_theme, .theme(..)
A drawn menu, tooltip or titlebar instead of the OS'spatterns.pattern(..), Application::patterns, [patterns]

Element style

Style methods apply to native controls where the control can express them: a fill becomes bezelColor on a macOS button, a text colour contentTintColor, a font an NSFont.

// Element style on a native control: tint and font map to bezelColor and NSFont
// on macOS; a radius AppKit cannot draw is ignored with a dev warning.
.child(
    button("rename", "Rename…")
        .dispatch(Rename)
        .bg(theme::forest())
        .text_color(tokens.text.on_accent)
        .font(tokens.fonts.rounded.clone()),
)

From examples/overrides/src/lib.rs, line 51.

A property the native control cannot draw, such as a corner radius on an NSButton, is ignored, and a dev build prints one warning that names the fix. Take that warning as the signal to move to mapping, not to stack more style on top. Use the colour from a token or a theme file; a literal in view code fails nd check.

Per platform

.on_macos, .on_linux, .on_windows and .on_web take a closure that runs only on that platform, so the call site needs no cfg. Methods that only mean something on one OS, such as bezel, exist on every target and are ignored elsewhere.

// Per platform: glass bezel on macOS, accent style on Windows, pill on Linux.
button("share", "Share")
    .icon(Icon::Share)
    .dispatch(Share)
    .on_macos(|b| b.bezel(Bezel::Glass))
    .on_windows(|b| b.accent())
    .on_linux(|b| b.rounded_full().px_4())
    .on_web(|b| b.outline()),

From examples/overrides/src/lib.rs, line 31.

Use it for a real difference in what the OS offers (a glass bezel exists on macOS 26, an accent button style on Windows). Do not use it to give every platform the same pixels; that undoes the native look the framework exists to keep. When two branches differ only in colour or radius, the theme is the better place.

Mapping

A mapping override changes which implementation draws an element. .drawn() forces the composed implementation on every OS, which takes every style override the native control cannot. Today it is on slider. .native(Mapping::..) picks another native control for the same element: select can be a pop-up, a segmented control or a radio group.

// Mapping override: the composed (drawn) slider on every OS, for a look the
// native control cannot express.
.child(slider("volume", self.volume).range(0.0..=1.0).drawn().h(px(28.)).rounded_full().on_change(
    cx.listener(|this, e: &SliderChanged, _window, cx| {
        this.volume = e.value;
        cx.notify();
    }),
))
// Mapping override: a picker shown as a segmented control instead of a pop-up.
.child(
    select("sort", ["Name", "Date", "Size"])
        .selected(self.sort)
        .native(Mapping::Segmented)
        .on_change(cx.listener(|this, e: &SelectChanged, _window, cx| {
            this.sort = e.index;
            cx.notify();
        })),
)

From examples/overrides/src/lib.rs, line 62.

A drawn control looks the same everywhere and does not follow OS restyling, so use .drawn() when the look is the point. Pick .native(..) when the OS has a control that fits.

Theme

Theme overrides change tokens (see Theming). They stack, and a deeper layer wins over a shallower one:

/// The app with its app-level overrides.
pub fn app() -> Application {
    Application::new(nd::manifest!())
        // App-level theme override, applied on top of the platform preset.
        .theme(theme::app())
        // App-level pattern override; nativedesktop.toml [patterns] sits below this.
        .patterns(Patterns { tooltips: Pattern::Drawn, ..Patterns::default() })
}

From examples/overrides/src/lib.rs, line 105.

The app layer lives in theme.rs, the only file in the example that names colours:

//! The only place this app names colours. `nd check` allows colour literals in theme files.

use nd::prelude::*;

pub fn app() -> ThemeOverride {
    ThemeOverride::new().accent(rgb(0x3d6bff)).radius_scale(1.25)
}

pub fn warm(theme: ThemeRefinement) -> ThemeRefinement {
    theme.accent(rgb(0xff8a3d)).radius_scale(1.5)
}

pub fn warm_glass_tint() -> Hsla {
    rgba(0xff8a3d33).into()
}

pub fn forest() -> Hsla {
    rgb(0x2f6f4f).into()
}

From examples/overrides/src/theme.rs, line 1.

The subtree layer wraps a tinted glass container, so its accent button and radii differ from the rest of the window:

// Subtree theme override: everything inside uses a warm accent and larger radii.
.child(
    glass()
        .variant(GlassVariant::Clear)
        .tint(theme::warm_glass_tint())
        .rounded_xl()
        .p_4()
        .gap_2()
        .theme(theme::warm)
        .child(text("Tinted glass with its own accent"))
        .child(
            h_flex().gap_2().child(button("ok", "Accept").primary()).child(button("no", "Decline")),
        )
        // Pattern override on one element: this context menu is drawn, not an NSMenu.
        .context_menu(|menu, _window, _cx| menu.item("Rename…", Rename).item("Share", Share))
        .pattern(Pattern::Drawn),
),

From examples/overrides/src/lib.rs, line 82.

radius_scale multiplies down the layers: an app scale of 1.25 and a subtree scale of 1.5 give 1.875. On macOS the accent reaches native controls inside the subtree as their bezel colour, and a forced appearance sets the subtree's NSAppearance. For a whole-app recolour that users can edit, ship a theme.toml instead of code (see Theming).

Patterns

A pattern is a structure the OS draws itself: a menu, a tooltip, a popover, a titlebar. A pattern override swaps the OS's version for one nd draws. Set it, from narrowest to widest:

A custom titlebar is built with titlebar(). It reserves the room the OS controls need, so no margin is tuned by hand:

// Pattern override: a custom titlebar even on macOS. `titlebar()` reserves the OS
// controls' side and size (traffic lights left on macOS, caption buttons right on
// Windows, nothing on Hyprland), so no margin is tuned by hand.
.child(
    titlebar()
        .title("Overrides")
        .trailing(
            // Per platform: glass bezel on macOS, accent style on Windows, pill on Linux.
            button("share", "Share")
                .icon(Icon::Share)
                .dispatch(Share)
                .on_macos(|b| b.bezel(Bezel::Glass))
                .on_windows(|b| b.accent())
                .on_linux(|b| b.rounded_full().px_4())
                .on_web(|b| b.outline()),
        )
        // Coordinates instead of the default slot, macOS only.
        .on_macos(|t| t.controls(window_controls().at(point(px(18.), px(20.))))),
)

From examples/overrides/src/lib.rs, line 23.

Traffic lights sit on the left on macOS, caption buttons on the right on Windows, GNOME's button-layout decides on Linux, and Hyprland has none. See Patterns for what each structure maps to.

When not to override

Run the example's tests with cargo test -p overrides. The appearance audit checks that its colours are tokens (see Theming); the macOS goldens compare the showcase in light and dark and need Screen Recording permission for the test process.