Docs

Platform-specific code

One NSH bundle runs on iOS, Android, macOS, Windows and Linux (and in Mac Catalyst), and in browsers through the web runtime. Most of an app needs no platform checks at all: elements, styles, layout and modules behave the same everywhere, and the components you draw from them look the same everywhere. When something really is specific to one platform, NSH gives it a clear place, so platform code stays visible and contained:

RustTSXFor
Builder methods (.ripple(..), .input_accessory(..))ios={{…}}, android={{…}}, desktop={{…}}, web={{…}}Props only one platform has.
on_back_press, nsh_core::element::desktopnsh/mobile, nsh/desktopAPIs only mobile platforms or only desktops have.
os(), idiom(), is_desktop()platform.os, platform.selectChoosing values or code per platform.
.when(os() == Os::Ios, ..)ios: / android: / web: classesStyles per platform.

#Platform-only props

In Rust, a prop one platform has is a builder method like any other (.ripple(..), .elevation(..) for Android, .input_accessory(..) for iOS, .window_drag(..) for the desktops); the other platforms leave it aside. In TSX, props that exist on one platform only are grouped in an object named after it. Each element accepts ios, android, desktop (macOS, Windows, Linux, and browsers in the desktop idiom) and web (browsers); on other platforms the object is ignored, and on its own platform only its keys are applied. A desktop browser applies both desktop and web.

use nsh_base::dialog;
use nsh_core::{
    Ripple,
    element::{AutoFill, Orientation}, prelude::*,
};

view()
    .on_press(open)
    .active(|s| s.opacity(0.6))
    .ripple(Ripple {
        color: Some(rgba(0x0000001f).into()),
        ..Ripple::default()
    })
    .elevation(px(4.0))
    .p_4()
    .rounded(px(12.0))
    .bg(white())
    .child(text("Open"))

input().placeholder("Code").auto_fill(AutoFill::OneTimeCode)

when(shown, || {
    dialog()
        .supported_orientations([Orientation::Portrait])
        .status_bar_translucent(true)
        .child(…)
})

The TSX objects are reactive like any prop; a key removed from the object is cleared.

What exists today:

RustTSXOnDescription
.ripple(Ripple { .. })android={{ ripple }}every elementThe material ripple while pressed: color, borderless, radius, foreground.
.elevation(..)android={{ elevation }}every elementThe material shadow, in points, from the view’s shape.
android={{ accessibilityLiveRegion }}every element"none", "polite" or "assertive": TalkBack announces the view’s changes.
.status_bar_translucent(..)android={{ statusBarTranslucent }}DialogThe dialog’s content reaches under the status bar.
.input_accessory(..)ios={{ inputAccessoryViewID }}InputThe InputAccessoryView shown over the keyboard.
.auto_fill(..)ios={{ textContentType }}InputiOS’s names for autofill types (Rust’s AutoFill covers both platforms).
.supported_orientations(..)ios={{ supportedOrientations }}DialogThe orientations it allows.
.back_forward_gestures(..)ios={{ allowsBackForwardNavigationGestures }}WebViewSwipe back and forward in the page history.
.header_right_system_item(..)ios={{ headerRightSystemItem }}NavPageUIKit’s own bar button at the end of the header, pressing headerRight.
.back_forward_gestures(..)desktop={{ allowsBackForwardNavigationGestures }}WebViewmacOS: two-finger trackpad swipes go back and forward in the page history; Windows: touchscreen swipes; Linux: touchpad swipes.
.window_drag(..)desktop={{ windowDrag }}every elementPressing it drags the window, as the system’s title bar would (see nsh/desktop).

In TSX, desktop={{…}} and web={{…}} are accepted everywhere. The types (IOSProps, AndroidProps, DesktopProps and their per-element forms) tell your editor what each element takes.

Something two or more platforms do is a top-level prop (in Rust a method of the same name), applied where it means something: NavPage’s title, headerStyle, headerLargeTitle and headerBackTitle set up the system header of iOS and Android, its header slot is the header of the desktops and the web, and Dialog’s presentation picks iOS’s sheets. AlertDialog.show’s closeOnBackdropPress, pagingEnabled, refreshing, hitSlop and the rest work on every platform.

#Platform modules

In Rust, the APIs only one platform has are functions of nsh_core::prelude (on_back_press, input_accessory_view) and of nsh_core::element::desktop (set_title_bar, title_bar, close_window); elsewhere they do nothing. In TSX, nsh/mobile holds the APIs of iOS and Android (some of them one platform’s), nsh/desktop those of the desktops. Both can be imported on every platform, so one bundle runs everywhere: on other platforms their functions return at once and their components take no room. Each exports isAvailable, which is true on its own platforms; platform.os tells them apart.

#nsh/mobile

isAvailable is true on iOS (with Mac Catalyst) and Android. Both platforms have TabBar, the system’s tab bar.

#BackHandler (Android)

RustTSXDescription
on_back_press(listener)BackHandler.onBackPress(listener)Listens to the back button after dialogs, before stacks; return true when handled. The listener goes with the component (or effect) that added it; in TSX the call also returns a function that stops it earlier.
BackHandler.exitApp()Leaves the app.
use nsh_core::prelude::*;

fn player() -> impl IntoElement {
    let full_screen = signal(false);
    // Back leaves full screen before it leaves the page. The listener goes
    // with the view.
    on_back_press(move || {
        if !full_screen.get_untracked() {
            return false;
        }
        full_screen.set(false);
        true
    });
    // …
}

The back button is handled by the top dialog first, then the back listeners (most recent first), then the top navigation stack, and the app exits when nothing handled it. See Navigation.

#InputAccessoryView (iOS)

RustTSXDescription
input_accessory_view(native_id)InputAccessoryViewA bar over the keyboard of the inputs naming its native id (.input_accessory(..), ios={{ inputAccessoryViewID }}).

See Forms and the keyboard.

#nsh/desktop

Rust apps draw their own title bars with nsh_core::element::desktop (Building with Rust). TSX’s module:

ExportDescription
WindowThe window: setTitle, setTitleBar("hidden") for a title bar of the app’s own, minimize, toggleMaximize, toggleFullScreen, close.
titleBarThe hidden title bar’s height and the room the system’s window controls take (reactive).
MenusetApplicationMenu(items) (the menu bar, with shortcuts) and popup(items, at?) (context menus).
isAvailabletrue on macOS, Windows and Linux.

See nsh/desktop.

#The status bar

On both platforms the status bar belongs to the page or dialog on top, as in UIKit, where the topmost view controller decides it: NavPage and Dialog take statusBar (in Rust .status_bar(StatusBarStyle::Light), or a StatusBarSetting for the other fields) with foreground ("auto", "light" for dark backgrounds, "dark" for light ones), hidden and animated. The page or dialog shown most recently wins, field by field, and the one below it applies again when it goes away. nsh.json’s "statusBar" is the app’s default under them.

use nsh_base::nav_page;
use nsh_core::prelude::*;

fn photo_viewer() -> impl IntoElement {
    nav_page()
        .header_visible(false)
        .status_bar(StatusBarStyle::Light)
        .child(view().flex_1().bg(black()))
}

In an app embedded in a native one, the host app’s view controller decides the status bar on iOS. An app that declares "capabilities" in nsh.json needs "statusBar": true to change it.

#platform.os and platform.select

os() in Rust and the platform constant from nsh in TSX tell you where the app runs: Os::Ios, Os::Android, Os::MacOs, Os::Windows, Os::Linux or Os::Web, and platform.os "ios", "android", "macos", "windows", "linux" or "web" (Mac Catalyst is "ios", with platform.catalyst; the macOS backend is "macos", the Linux backend "linux" and the Windows backend "windows", and their elements apply their desktop props; browsers are "web", and apply desktop props too when the idiom is "desktop"). See Device and environment for the version, the idiom and the rest.

use nsh_core::{element::Os, prelude::*};

if os() == Os::Android {
    // …
}

#platform.select

In Rust a match os() chooses the value, with is_desktop() for the desktops. In TSX, platform.select(spec) returns the value for this platform. It looks for the OS’s own key, then mobile (iOS, Android, phone and tablet browsers) or desktop (macOS, Windows, Linux, desktop browsers), then native (every platform but the web), then default:

use nsh_core::{element::Os, prelude::*};

fn card() -> View {
    let card = view().rounded(px(12.0)).bg(white());
    match os() {
        Os::Ios => card.shadow_md(),
        Os::Android => card.border_1().border_color(rgb(0xe5e7eb)),
        _ => card,
    }
}

let hint = match os() {
    Os::Ios => "Swipe down to close",
    Os::Android => "Press back to close",
    _ => "Close",
};

It returns undefined when nothing matches and there is no default.

#Rendering different components

Choose components the same way, at module level (TSX) when the choice never changes:

fn spinner() -> AnyElement {
    if os() == Os::Ios {
        ios_spinner().into_element()
    } else {
        material_spinner().into_element()
    }
}

or inline with when / <If>:

when(os() == Os::Ios, || input_accessory_view("bar").child(…))
    .otherwise(|| text("Android has no input accessories."))

#Platform variants in Tailwind

Rust styles per platform with .when(os() == Os::Ios, |v| ..); in TSX, the ios:, android: and web: variants apply classes on one platform:

view()
    .rounded_xl()
    .bg(white())
    .p_4()
    .when(os() == Os::Ios, |v| v.shadow_md())
    .when(os() == Os::Android, |v| v.border_1().border_color(rgb(0xe4e4e7)))

#Prefer shared behavior

Before reaching for a platform check, consider whether the difference matters to your users. Elements and styles already map to each platform’s native behavior where it should differ (scroll physics, text rendering, keyboards, accessibility), while staying the same where users expect consistency (your app’s own look). Platform checks are best kept for platform conventions users rely on: Android’s back button and toasts, iOS’s sheets and large titles, autofill names, Android’s ripple.