Elements

View

<View> is the building block of every screen: a native container (a UIView on iOS and Mac Catalyst, a ViewGroup on Android) that lays out its children with flexbox or grid, draws a background, borders, shadows and gradients, and handles presses. Buttons, cards, rows and screens are all views.

el-view.tsx Edit in Playground Open in new tab
el-view.tsxLive
import { mount, signal, Text, View } from "nsh";
import { ui } from "./palette";

function App() {
  const presses = signal(0);
  const size = signal("");
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 16 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>view</Text>

      <View
        onPress={() => presses.update((n) => n + 1)}
        activeOpacity={0.5}
        style={{
          ...ui().accentFill,
          borderRadius: 12,
          padding: 16,
          alignItems: "center",
        }}
      >
        <Text style={{ color: ui().onAccent, fontSize: 16, fontWeight: "600" }}>activeOpacity · pressed {presses()}×</Text>
      </View>

      <View
        onPress={() => presses.update((n) => n + 1)}
        underlayColor={ui().border}
        style={{ ...ui().card, backgroundImage: "none", borderRadius: 16, padding: 16 }}
      >
        <Text style={{ color: ui().text, fontSize: 16 }}>underlayColor (tap and hold)</Text>
      </View>

      <View class="flex-row gap-3">
        <View class="flex-1 h-20 rounded-xl bg-[#ff6170] active:bg-[#d94d5b] items-center justify-center" onPress={() => {}}>
          <Text class="text-[#13160c] font-semibold">active:</Text>
        </View>
        <View class="flex-1 h-20 rounded-xl bg-[#45d49a] items-center justify-center shadow-lg">
          <Text class="text-[#0c0d0f] font-semibold">shadow-lg</Text>
        </View>
        <View
          class="flex-1 h-20 rounded-xl items-center justify-center"
          style={{ backgroundImage: `linear-gradient(135deg, ${ui().violet}, ${ui().info})` }}
        >
          <Text class="font-semibold" style={{ color: ui().page }}>gradient</Text>
        </View>
      </View>

      <View
        onLayout={(e) => size.set(`${Math.round(e.width)} × ${Math.round(e.height)} pt`)}
        style={{ flex: 1, borderRadius: 12, borderWidth: 2, borderStyle: "dashed", borderColor: ui().faint, alignItems: "center", justifyContent: "center" }}
      >
        <Text style={{ color: ui().muted }}>onLayout: {size()}</Text>
      </View>
    </View>
  );
}

mount(App);
import { mount, signal, Text, View } from "nsh";

function App() {
  const liked = signal(false);
  return (
    <View style={{ flex: 1, padding: 24, gap: 16, backgroundColor: "#f6f8fa" }}>
      <View
        style={{
          backgroundColor: "#ffffff",
          borderRadius: 16,
          padding: 16,
          boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
          flexDirection: "row",
          alignItems: "center",
          gap: 12,
        }}
      >
        <View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: "#8250df" }} />
        <Text style={{ flex: 1, fontSize: 16 }}>A card</Text>
        <View onPress={() => liked.set(!liked())} activeOpacity={0.5} hitSlop={8}>
          <Text style={{ fontSize: 22 }}>{liked() ? "♥" : "♡"}</Text>
        </View>
      </View>
    </View>
  );
}

mount(App);

#Layout

A view lays out its children with flexbox, with these defaults: flexDirection: "column", flexShrink: 0, alignContent: "flex-start", position: "relative". display: "grid" switches to CSS grid and display: "block" to block layout. Sizes are in points. See layout and the style reference.

The Tailwind flex class follows CSS rather than the style default: class="flex" is a row. Use flex-col for a column.

#Props

<View> has no props of its own beyond the common props: style (ViewStyle), class, classList, disabled, the press props (onPress, onLongPress, onPressIn, onPressOut, delayLongPress, activeOpacity, underlayColor, underlayOpacity, hitSlop, pointerEvents), onHoverIn / onHoverOut, onLayout, ref, nativeID, testID, the gesture props (pan, pinch, rotation and their events), the touch and responder props, the accessibility props and ios / android / desktop.

#Press feedback

A pressable view is any view with a press handler. Three kinds of feedback run natively while the finger is down, with no JS round trip:

// Fade the whole view.
<View onPress={open} activeOpacity={0.6}>…</View>

// Show a background and fade the children (default 0.85).
<View onPress={open} underlayColor="#e5e7eb" underlayOpacity={0.7}>…</View>

// Change any style while pressed with active: classes.
<View onPress={open} class="bg-blue-600 active:bg-blue-800 active:scale-95">…</View>

On Android, android={{ ripple: { color: "#0000001f" } }} adds the material ripple (borderless, radius and foreground adjust it).

disabled removes the press listeners from the native view (no press, no feedback) and applies disabled: classes:

<View onPress={submit} disabled={!valid()} class="bg-green-600 disabled:opacity-40">…</View>

#Events

EventPayloadWhen
onPressnoneA tap ended inside the view.
onLongPressnoneThe finger stayed down for delayLongPress (500 ms).
onPressIn / onPressOutnoneA finger went down / lifted or left.
onHoverIn / onHoverOutnoneThe mouse came over the view (or a view inside it) / left it or the window; macOS, Linux and GPUI (hover).
onLayout{ x, y, width, height }The frame relative to the parent changed.
onPanStart / onPanEndPanEventA drag bound with pan started / ended.
onPinchStart / onPinchEnd{ scale, velocity }A pinch bound with pinch started / ended.
onRotationStart / onRotationEnd{ rotation, velocity }A rotation bound with rotation started / ended.
onAccessibilityAction{ actionName }A custom accessibility action ran.

Touch and responder events: see common props.

#Ref methods

A plain view has no commands of its own. ref gives you the YNode, e.g. to pass to AccessibilityInfo.setAccessibilityFocus (nsh/accessibility).

#Views inside text

A <View> placed inside a <Text> is an inline attachment: it sits on the baseline and wraps with the words, and it takes presses of its own. See text.

#Platform notes

  • boxShadow and the shadow props (shadowColor, shadowOpacity, shadowRadius, shadowOffset) draw on both platforms; android={{ elevation }} gives Android’s material shadow instead. On Android a shadow needs a background color (a view with a transparent background casts none).
  • filter on views: iOS supports only opacity(), brightness() up to 1 and drop-shadow() (images take every color filter and blur()); Android supports them all from API 31, and only the color filters below it.
  • backdropFilter: "blur(…)" is a system material on iOS (an approximation) and needs API 31 on Android.
  • Android has no skewX / skewY transforms.
  • borderCurve: "continuous" draws corners that ease into the edges (as iOS app icons), on both platforms.
  • The cursor style applies where there is a pointer (Mac Catalyst, iPad with a pointer, desktops).