Components

Mobile UI

nsh/ui/mobile is nsh’s UI set for phones and tablets: buttons, cards, lists, text fields, sheets, safe areas, the keyboard, the status bar, system dialogs and navigation. Import what you need:

import { Button, Card, Group, Sheet, Stack, Screen, createNavigator } from "nsh/ui/mobile";

Use it on top of the host elements (<view>, <text>, <input>, …). The elements are the building blocks every UI set uses. The components are a finished mobile look built from those elements.

#How the components work

The components are written in Rust, not in JavaScript. In your app a <Button> is still one JSX node. Its props go to the native core, and the core renders the button from host elements in the same layout pass as the rest of your UI. Its events come back to your handlers. This has a few practical consequences:

  • The same look on iOS and Android. A component draws itself with nsh’s own views, not UIKit or Android widgets, so a Button looks the same on both platforms. The exceptions are Alert and ActionSheet, which show the platform’s system dialogs.
  • Interaction stays native. Press feedback, the drag of a Sheet and the repeat of a held Stepper button run in the core or on the UI thread. JS only receives the events (onPress, onChange, onClose, …).
  • Typed from the source. The TypeScript declarations in .nsh/types/nsh/ui/mobile.d.ts are generated from the Rust props, so your editor shows each prop and its documentation.
  • Your content stays yours. Slots (header, footer), children and list rows are your JSX. The component positions them but never creates or changes them, so their signals, handlers and refs work as usual.

Every component accepts class, classList, style, testID, onLayout and ref on its root view (ComponentHostProps). Your class is applied after the component’s own styles, so it overrides them:

<Button title="Continue" class="rounded-full" onPress={next} />
<Card class="mx-4" variant="outlined">…</Card>

#Components

ComponentWhat it is
ButtonA press target with a label: primary, secondary, danger or plain
CardA rounded surface with an optional header and footer
GroupSettings-style rows in a rounded box, divided by hairlines
ListViewA virtualized list of your rows, plain or inset
SafeAreaViewA view padded by the safe area, a screen’s header or a modal’s insets
SelectListA scrolling list of options with a check mark on the chosen one
SheetA bottom sheet over a dimmed backdrop, dragged down to close
StatusBarThe status bar’s content color and visibility, rendered or set by function
StepperA number with − and + buttons; holding a button repeats
TextFieldA labeled text input with a clear button
KeyboardAvoidingViewKeeps content outside scroll views above the software keyboard

Modules and navigation:

ExportWhat it is
AlertAlert.alert(title, message?, buttons?, options?): a system alert, resolves with the button’s index
ActionSheetActionSheet.show(options): a system action sheet, resolves with the action’s index
KeyboardKeyboard.dismiss() and the reactive keyboard() accessor
NavigationcreateNavigator, Stack, Screen and Modal: native navigation stacks and modals

The set also re-exports three environment accessors from the runtime: safeArea(), keyboard() and useSafeAreaInsets() (see SafeAreaView and Keyboard).

#Theming

All components use colors from one theme and never use fixed color values. Each color has a light and a dark value. When the system switches between light and dark appearance, the components change color natively, with no render and no JS.

The default theme is gpui-kit’s default theme, a neutral black-and-white palette, with mobile corner radii. To use your own theme, set "theme" in nsh.json. Its value is the path of a theme file, relative to nsh.json:

{
  "name": "My App",
  "id": "com.example.myapp",
  "theme": "brand-theme.json"
}

You can also write the theme’s content inline as the value. The format is gpui-kit’s theme file format. Each entry in themes sets the colors for one appearance (mode: "light" or "dark"; without mode, an entry is light):

{
  "themes": [
    {
      "mode": "light",
      "radius": 8,
      "radius.lg": 14,
      "font.size": 16,
      "colors": {
        "primary.background": "#7c3aed",
        "primary.foreground": "white",
        "border": "neutral-200"
      }
    },
    {
      "mode": "dark",
      "colors": {
        "primary.background": "#a78bfa",
        "primary.foreground": "neutral-950"
      }
    }
  ]
}
  • Your theme is applied over the default theme, so you only list what you change.
  • Colors are CSS colors (#7c3aed, rgb(…), white) or names from Tailwind’s default palette (neutral-900, red-500). Custom colors from nsh.json’s tailwind key are not available here.
  • radius is the corner radius of controls (buttons, inputs, stepper buttons). radius.lg is the radius of large surfaces (cards, groups, inset lists, sheets). font.size is the base font size of button labels, card headers and the stepper. All values are in points.
  • If you leave out a pressed color (primary.active.background, secondary.active.background, danger.active.background, …), nsh derives it from the base color: 12% darker in light mode, 12% lighter in dark mode.
  • Keys that only gpui-kit’s desktop components use (sidebars, title bars, …) are ignored, so you can use a gpui-kit theme file as it is.
  • The bundler checks the theme when it builds. A missing file, invalid JSON, an unknown mode or a value that is not a color fails the build with an nsh.json: theme: … error.

These are the theme keys the mobile components use, with their default light and dark values:

KeyLightDarkUsed by
backgroundwhiteneutral-950ListView, SelectList and TextField backgrounds, the Sheet panel
foregroundneutral-950neutral-50Text in ListView, SelectList and Stepper; Plain buttons
borderneutral-200neutral-800Dividers, Card borders
primary.background / .active.background / .foregroundneutral-900 / neutral-950 / neutral-50neutral-50 / neutral-200 / neutral-900Primary buttons; SelectList check mark, TextField selection (primary.background)
secondary.background / .active.background / .foregroundneutral-200 / neutral-300 / neutral-900neutral-800 / #212121 / neutral-50Secondary buttons, Stepper buttons
danger.background / .active.background / .foregroundred-500 / red-600 / neutral-50red-400 / red-500 / red-600Danger buttons
accent.backgroundneutral-100neutral-800Plain buttons while pressed
muted.backgroundneutral-100neutral-800Inset ListView page, Sheet grabber, TextField clear button
muted.foregroundneutral-500neutral-400Group captions, TextField labels and placeholders
group_box.background / .foreground#f5f5f5 / #171717neutral-950 / neutral-50Card and Group surfaces
input.borderneutral-200#2f2f2fTextField border
ringneutral-400neutral-500TextField border while focused
list.active.background#bfdbfe33#1e40af33SelectList row while pressed
overlay#0000000d#00000033Sheet backdrop