NativeDesktop

Overrides

Change how a native default looks or behaves, from one element's style up to the whole app, and pick the right layer for each change.

Every component starts with the OS's own default. Start with no overrides, and add one only when you want a specific difference. Each layer below changes less than the one after it, so pick the smallest layer 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.
Runs Overrides in this page as WebAssembly.
Overrides on macOS

Captured on macOS 27.0, 2026-10-22.

Overrides on Linux

Captured on NixOS 26.11 (Zokor), Hyprland, 2026-10-22.

Overrides on Windows

Captured on Windows 11 26H2, 2026-10-22.

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 colors, 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 a native control when the control can express them. On a macOS button, a fill becomes bezelColor, a text color becomes contentTintColor, and a font becomes 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(theme::forest_ink())
        .font(tokens.fonts.rounded.clone()),
)

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

When the native control can't draw a property, such as a corner radius on an NSButton, nd ignores it, and a dev build prints one warning that names the fix. When you see that warning, move to mapping instead of stacking more style on top. Take the color from a token or a theme file. A literal color 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 mean something on only one OS, such as bezel, exist on every target, and the other targets ignore them.

// 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 when the OSes offer different things: a glass bezel exists on macOS 26, and an accent button style exists on Windows. Don't use it to give every platform the same pixels, because that undoes the native look that the framework exists to keep. When two branches differ only in color or radius, put the difference in the theme instead.

Mapping

A mapping override changes which implementation draws an element. .drawn() forces the composed implementation on every OS, and that implementation accepts every style override that the native control can't. Today slider has it. .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 doesn't follow OS restyling. Use .drawn() when the exact look matters most, and use .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 is in theme.rs, the only file in the example that names colors:

//! 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()
}

pub fn forest_ink() -> Hsla {
    rgb(0xffffff).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 color, and a forced appearance sets the subtree's NSAppearance. To recolor the whole app in a way that users can edit, ship a theme.toml instead of code (see Theming).

Patterns

A pattern is a structure that the OS draws itself, such as a menu, tooltip, popover, or titlebar. A pattern override swaps the OS's version for one that nd draws. You can set it in these places, from narrowest to widest:

Build a custom titlebar with titlebar(). It reserves the room that the OS controls need, so you don't tune any margin 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.

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

When not to override

To run the example's tests, use cargo test -p overrides. The appearance audit checks that its colors are tokens (see Theming). The macOS goldens compare the showcase in light and dark mode, and the test process needs Screen Recording permission.