APM

>Agent Skill

@wpank/native-ui

skilldevelopment

Building native mobile UIs with Expo Router and React Native. Covers routing, navigation, styling, native controls, animations, and platform conventions following Apple Human Interface Guidelines.

apm::install
$apm install @wpank/native-ui
apm::skill.md
---
name: native-ui
model: standard
version: 1.0.0
description: >
  Building native mobile UIs with Expo Router and React Native. Covers routing,
  navigation, styling, native controls, animations, and platform conventions
  following Apple Human Interface Guidelines.
tags: [expo, react-native, ios, mobile, navigation, native-ui]
---

# Native UI with Expo Router

Patterns and conventions for building native mobile applications with Expo Router and React Native.

## References

Consult these as needed:

- `./references/route-structure.md` — Route conventions, dynamic routes, groups, folder organization
- `./references/tabs.md` — Native tab bar with NativeTabs, iOS 26 features
- `./references/icons.md` — SF Symbols with expo-symbols, icon names, animations, weights
- `./references/controls.md` — Native iOS controls: Switch, Slider, SegmentedControl, DateTimePicker
- `./references/visual-effects.md` — Blur effects (expo-blur) and liquid glass (expo-glass-effect)
- `./references/animations.md` — Reanimated: entering, exiting, layout, scroll-driven, gestures
- `./references/search.md` — Search bar with headers, useSearch hook, filtering patterns
- `./references/gradients.md` — CSS gradients via experimental_backgroundImage (New Architecture only)
- `./references/media.md` — Camera, audio, video, file saving
- `./references/storage.md` — SQLite, AsyncStorage, SecureStore
- `./references/webgpu-three.md` — 3D graphics and GPU visualizations with WebGPU/Three.js
- `./references/toolbar-and-headers.md` — Stack headers and toolbar with buttons, menus, search bars (iOS)
- `./references/form-sheet.md` — Form sheet presentation patterns

## Running the App

**Always try Expo Go first before creating custom builds.**

1. Start with `npx expo start` and scan the QR code
2. Test features in Expo Go
3. Only create custom builds when required

### When Custom Builds Are Required

Use `npx expo run:ios/android` or `eas build` only for:

- Local Expo modules (custom native code in `modules/`)
- Apple targets (widgets, app clips via `@bacons/apple-targets`)
- Third-party native modules not in Expo Go
- Custom native configuration beyond `app.json`

Expo Go supports all `expo-*` packages, Expo Router, Reanimated, Gesture Handler, push notifications, and deep links out of the box.


## Installation

### OpenClaw / Moltbot / Clawbot

```bash
npx clawhub@latest install native-ui
```


---

## Code Style

- Escape nested backticks and quotes correctly
- Always use import statements at the top of the file
- Use kebab-case for file names: `comment-card.tsx`
- Remove old route files when restructuring navigation
- No special characters in file names
- Configure `tsconfig.json` path aliases; prefer aliases over relative imports

## Routes

See `./references/route-structure.md` for detailed conventions.

- Routes belong in the `app` directory
- Never co-locate components, types, or utilities in `app/` — this is an anti-pattern
- Always have a route matching `/`, possibly inside a group route

## Library Preferences

| Use | Instead of |
|-----|------------|
| `expo-audio` | `expo-av` |
| `expo-video` | `expo-av` |
| `expo-symbols` | `@expo/vector-icons` |
| `react-native-safe-area-context` | RN SafeAreaView |
| `process.env.EXPO_OS` | `Platform.OS` |
| `React.use` | `React.useContext` |
| `expo-image` | Intrinsic `img` element |
| `expo-glass-effect` | Custom glass backdrops |

Never use deprecated modules: Picker, WebView, SafeAreaView, AsyncStorage (from RN core), or legacy `expo-permissions`.

---

## Responsiveness

- Wrap root components in a scroll view
- Use `<ScrollView contentInsetAdjustmentBehavior="automatic" />` instead of `<SafeAreaView>`
- Apply `contentInsetAdjustmentBehavior="automatic"` to FlatList and SectionList too
- Use flexbox instead of Dimensions API
- Prefer `useWindowDimensions` over `Dimensions.get()` for screen measurement

## Behavior

- Use `expo-haptics` conditionally on iOS for delightful interactions
- Use views with built-in haptics (`<Switch />`, `@react-native-community/datetimepicker`)
- First child of a Stack route should almost always be a ScrollView with `contentInsetAdjustmentBehavior="automatic"`
- Prefer `headerSearchBarOptions` in Stack.Screen options for search bars
- Use `<Text selectable />` on data that users may want to copy
- Format large numbers: 1.4M, 38k
- Never use intrinsic elements (`img`, `div`) outside webviews or Expo DOM components

---

## Styling

Follow Apple Human Interface Guidelines.

### General Rules

- Prefer flex gap over margin and padding
- Prefer padding over margin
- Always account for safe area via stack headers, tabs, or `contentInsetAdjustmentBehavior="automatic"`
- Ensure both top and bottom safe area insets are handled
- Inline styles preferred over `StyleSheet.create` unless reusing styles
- Add entering/exiting animations for state changes
- Use `{ borderCurve: 'continuous' }` for rounded corners (not capsule shapes)
- Use navigation stack title instead of custom text headers
- On ScrollView, use `contentContainerStyle` for padding/gap (avoids clipping)
- CSS and Tailwind are not supported — use inline styles

### Text Styling

- Add `selectable` prop to `<Text/>` elements showing important data or errors
- Use `{ fontVariant: 'tabular-nums' }` on counters for alignment

### Shadows

Use CSS `boxShadow` style prop. Never use legacy RN shadow or elevation styles.

```tsx
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
```

Inset shadows are supported.

---

## Navigation

### Link

Use `<Link href="/path" />` from `expo-router` for navigation.

```tsx
import { Link } from 'expo-router';

<Link href="/path" />

<Link href="/path" asChild>
  <Pressable>...</Pressable>
</Link>
```

Include `<Link.Preview>` whenever possible to follow iOS conventions. Add context menus and previews frequently.

### Stack

- Always use `_layout.tsx` files to define stacks
- Use `Stack` from `expo-router/stack` for native navigation stacks
- Set page titles in Stack.Screen options: `options={{ title: "Home" }}`

### Context Menus

Add long-press context menus to Link components:

```tsx
<Link href="/settings" asChild>
  <Link.Trigger>
    <Pressable><Card /></Pressable>
  </Link.Trigger>
  <Link.Menu>
    <Link.MenuAction title="Share" icon="square.and.arrow.up" onPress={handleShare} />
    <Link.MenuAction title="Block" icon="nosign" destructive onPress={handleBlock} />
    <Link.Menu title="More" icon="ellipsis">
      <Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} />
      <Link.MenuAction title="Delete" icon="trash" destructive onPress={() => {}} />
    </Link.Menu>
  </Link.Menu>
</Link>
```

### Link Previews

```tsx
<Link href="/settings">
  <Link.Trigger>
    <Pressable><Card /></Pressable>
  </Link.Trigger>
  <Link.Preview />
</Link>
```

Can be combined with context menus.

### Modal

Present a screen as a modal:

```tsx
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
```

Prefer this over custom modal components.

### Sheet

Present as a dynamic form sheet:

```tsx
<Stack.Screen
  name="sheet"
  options={{
    presentation: "formSheet",
    sheetGrabberVisible: true,
    sheetAllowedDetents: [0.5, 1.0],
    contentStyle: { backgroundColor: "transparent" },
  }}
/>
```

`contentStyle: { backgroundColor: "transparent" }` enables liquid glass on iOS 26+.

---

## Common Route Structure

Standard app layout with tabs and stacks:

```
app/
  _layout.tsx        — <NativeTabs />
  (index,search)/
    _layout.tsx      — <Stack />
    index.tsx        — Main list
    search.tsx       — Search view
```

**Root layout:**

```tsx
// app/_layout.tsx
import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";
import { Theme } from "../components/theme";

export default function Layout() {
  return (
    <Theme>
      <NativeTabs>
        <NativeTabs.Trigger name="(index)">
          <Icon sf="list.dash" />
          <Label>Items</Label>
        </NativeTabs.Trigger>
        <NativeTabs.Trigger name="(search)" role="search" />
      </NativeTabs>
    </Theme>
  );
}
```

**Shared group layout:**

```tsx
// app/(index,search)/_layout.tsx
import { Stack } from "expo-router/stack";
import { PlatformColor } from "react-native";

export default function Layout({ segment }) {
  const screen = segment.match(/\((.*)\)/)?.[1]!;
  const titles: Record<string, string> = { index: "Items", search: "Search" };

  return (
    <Stack
      screenOptions={{
        headerTransparent: true,
        headerShadowVisible: false,
        headerLargeTitleShadowVisible: false,
        headerLargeStyle: { backgroundColor: "transparent" },
        headerTitleStyle: { color: PlatformColor("label") },
        headerLargeTitle: true,
        headerBlurEffect: "none",
        headerBackButtonDisplayMode: "minimal",
      }}
    >
      <Stack.Screen name={screen} options={{ title: titles[screen] }} />
      <Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} />
    </Stack>
  );
}
```