Building UI
The procedure for authoring any JSX in a React Native repo built to the house
standard: Unistyles v3 stylesheets with theme tokens only, shared theme/ui
components instead of raw React Native primitives, every user-facing string in
i18n with accessibility props, and worklet-safe gesture code.
Procedure
- Ground yourself in this repo first. Token names, primitive names, and
i18n key namespaces differ between projects — inventing one is the most
common failure. Before writing anything:
- Read the theme tokens file (conventionally
src/theme/theme.ts) and note
the real token paths (theme.colors.*, theme.spacing.*, radii, shadows).
- Read the shared UI barrel (conventionally
src/theme/ui/index.ts) and list
what already exists. Do not write a component the kit already has.
- Open one existing screen and one existing shared primitive. That is the
house pattern, whatever this skill says.
- Read
AGENTS.md / CLAUDE.md if present.
- Decide where the component lives (see "Where a component belongs").
- Write styles with
StyleSheet.create(theme => ({...})) imported from
react-native-unistyles, using only tokens that exist in the theme file.
- Add every user-facing string (including a11y labels and hints) to the i18n
catalog and read it via
useTranslation.
- Add
accessibilityLabel, accessibilityRole, and where helpful
accessibilityHint and accessibilityState to every interactive element.
- Run lint and typecheck. See
validate-change for which checks match the scope.
Key files to locate (names vary by repo — verify, don't assume)
| What |
Conventionally |
How to confirm |
| Theme tokens (the only place color literals live) |
src/theme/theme.ts |
grep for the hex literals |
| Unistyles registration / theme types |
src/theme/unistyles.ts, src/theme/types.ts |
grep StyleSheet.configure |
| Shared UI barrel |
src/theme/ui/index.ts |
the barrel other modules import |
| User-facing copy |
i18n/<locale>.json |
grep an existing t('...') key |
| Toast adapter |
src/utils/toast.tsx |
grep showToast |
Unistyles v3
import { StyleSheet } from 'react-native-unistyles' — never from
react-native. Importing from react-native bypasses theming and silently
breaks light/dark.
const styles = StyleSheet.create(theme => ({ ... })) at the bottom of the
file, or in a colocated styles.ts when it gets long enough to bury the JSX.
Purely static stylesheets may use the object form.
- Theme tokens only. A hex or
rgb() in a component is a lint error.
- Values genuinely derived from runtime data (a measured height, an avatar
diameter from props) are the one acceptable inline exception.
- Never
style={[styles.x]} with a single element — use style={styles.x}.
- Never hand-order style properties; let
lint --fix sort them.
- Platform splits:
Platform.select for simple cases, .ios.tsx /
.android.tsx for complex ones.
For anything beyond this — variants, breakpoints, scoped themes, runtime,
withUnistyles, Reanimated integration, v2 migration — load the unistyles
skill, which carries the full offline docs.
Compose from the shared UI kit
Always compose from the repo's theme/ui primitives rather than raw
react-native components. Typical shape of such a kit:
- A
Text component with boolean shorthand props (title, subtitle,
caption, bold, center) — raw text outside it is a lint error.
- A
View with layout shorthands (flex1, row, center, middle) —
prefer these over one-off flex styles.
- A
Touchable / Button family — the pressable surface with themed styling.
- Modal and sheet surfaces. For new bottom sheets, load the
truesheet-usage
skill.
- Image and avatar wrappers.
Read the barrel before writing a new primitive. If what you need is genuinely
missing and is presentation-only with no module imports, add it to theme/ui
and export it from the barrel — do not fork a local copy inside a module.
Long lists use FlatList, never ScrollView + .map().
i18n + accessibility procedure
- Add keys to the i18n catalog under the feature's namespace.
- Read them with
useTranslation and pass translations to a11y props too —
accessibility copy is user-facing copy.
const { t } = useTranslation();
const selectLabel = t('settings.themePicker.selectTheme');
const label = `${selectLabel} ${displayName}`;
return (
<Touchable
accessible
button
accessibilityHint={selectLabel}
accessibilityLabel={label}
accessibilityRole="button"
accessibilityState={{ selected }}
style={styles.container}
Note the JSX prop order lint enforces: key first, then shorthand booleans,
then regular props alphabetically, then callbacks last.
Toast
Import the app-facing showToast adapter (conventionally utils/toast), never
the visual layer under theme/ui. The adapter carries defaults and typically
queues toasts fired before the provider mounts; importing the runtime layer
directly loses both.
Worklets and gestures
Call JS from a worklet with scheduleOnRN from react-native-worklets:
.onEnd(event => {
'worklet';
const shouldDismiss =
event.translationY > dismissThreshold ||
event.velocityY > dismissVelocityThreshold;
if (shouldDismiss) {
scheduleOnRN(onDismiss);
}
})
Never import runOnJS from react-native-reanimated — it races under the
worklets runtime these apps use. Values read inside worklets must be
useSharedValue, not useRef. Mark worklet callbacks with the 'worklet'
directive.
Where a component belongs
- Reusable, presentation-only, no module or Redux imports → the shared
theme/ui kit, exported from its barrel.
- Feature-specific → inside its module:
components/ for small pieces,
fragments/ for larger screen sections, screens/ for route-level
orchestration.
- Import other modules only via their barrel (
modules/<name>). ESLint
no-restricted-imports blocks internals and madge catches the cycles.
See add-feature for the full module anatomy. The layer rule: screens
orchestrate, fragments section, components present, hooks own logic. A component
that reaches for useAppDispatch or useAppSelector has taken on a job that
belongs to its screen's hook.
Danger zone — never do
- Never
import { StyleSheet } from 'react-native' — themes break silently.
- Never write a hex/rgb color in a component; colors live only in the theme file.
- Never use inline styles or single-element style arrays.
- Never put raw text outside the kit's
Text component.
- Never hardcode user-facing strings, including alerts and a11y copy.
- Never import the toast visual layer from feature code — use the adapter.
- Never use
runOnJS; use scheduleOnRN.
- Never call
setState synchronously in a useEffect body — initialize
useState with the right value instead.
- Never invent a theme token or i18n namespace you did not read from the repo.
Definition of done
1---2name: build-ui3description: Builds screens, components, modals, and sheets to the house React Native UI standard. Use when the user asks to build or add a screen, component, modal, sheet, button, or any JSX; when doing styling, theming, animation, or gesture work; or before writing any StyleSheet, translation key, or accessibility prop in a React Native project. Covers Unistyles v3 patterns, composing from the shared UI kit instead of raw RN primitives, the i18n + a11y procedure, and worklet rules.4---56# Building UI78The procedure for authoring any JSX in a React Native repo built to the house9standard: Unistyles v3 stylesheets with theme tokens only, shared `theme/ui`10components instead of raw React Native primitives, every user-facing string in11i18n with accessibility props, and worklet-safe gesture code.1213## Procedure14151. **Ground yourself in this repo first.** Token names, primitive names, and16 i18n key namespaces differ between projects — inventing one is the most17 common failure. Before writing anything:18 - Read the theme tokens file (conventionally `src/theme/theme.ts`) and note19 the real token paths (`theme.colors.*`, `theme.spacing.*`, radii, shadows).20 - Read the shared UI barrel (conventionally `src/theme/ui/index.ts`) and list21 what already exists. Do not write a component the kit already has.22 - Open one existing screen and one existing shared primitive. That is the23 house pattern, whatever this skill says.24 - Read `AGENTS.md` / `CLAUDE.md` if present.252. Decide where the component lives (see "Where a component belongs").263. Write styles with `StyleSheet.create(theme => ({...}))` imported from27 `react-native-unistyles`, using only tokens that exist in the theme file.284. Add every user-facing string (including a11y labels and hints) to the i18n29 catalog and read it via `useTranslation`.305. Add `accessibilityLabel`, `accessibilityRole`, and where helpful31 `accessibilityHint` and `accessibilityState` to every interactive element.326. Run lint and typecheck. See `validate-change` for which checks match the scope.3334## Key files to locate (names vary by repo — verify, don't assume)3536| What | Conventionally | How to confirm |37| --- | --- | --- |38| Theme tokens (the only place color literals live) | `src/theme/theme.ts` | grep for the hex literals |39| Unistyles registration / theme types | `src/theme/unistyles.ts`, `src/theme/types.ts` | grep `StyleSheet.configure` |40| Shared UI barrel | `src/theme/ui/index.ts` | the barrel other modules import |41| User-facing copy | `i18n/<locale>.json` | grep an existing `t('...')` key |42| Toast adapter | `src/utils/toast.tsx` | grep `showToast` |4344## Unistyles v34546- `import { StyleSheet } from 'react-native-unistyles'` — **never** from47 `react-native`. Importing from `react-native` bypasses theming and silently48 breaks light/dark.49- `const styles = StyleSheet.create(theme => ({ ... }))` at the bottom of the50 file, or in a colocated `styles.ts` when it gets long enough to bury the JSX.51 Purely static stylesheets may use the object form.52- Theme tokens only. A hex or `rgb()` in a component is a lint error.53- Values genuinely derived from runtime data (a measured height, an avatar54 diameter from props) are the one acceptable inline exception.55- Never `style={[styles.x]}` with a single element — use `style={styles.x}`.56- Never hand-order style properties; let `lint --fix` sort them.57- Platform splits: `Platform.select` for simple cases, `.ios.tsx` /58 `.android.tsx` for complex ones.5960For anything beyond this — variants, breakpoints, scoped themes, runtime,61`withUnistyles`, Reanimated integration, v2 migration — load the `unistyles`62skill, which carries the full offline docs.6364## Compose from the shared UI kit6566Always compose from the repo's `theme/ui` primitives rather than raw67`react-native` components. Typical shape of such a kit:6869- A `Text` component with boolean shorthand props (`title`, `subtitle`,70 `caption`, `bold`, `center`) — raw text outside it is a lint error.71- A `View` with layout shorthands (`flex1`, `row`, `center`, `middle`) —72 prefer these over one-off flex styles.73- A `Touchable` / `Button` family — the pressable surface with themed styling.74- Modal and sheet surfaces. For new bottom sheets, load the `truesheet-usage`75 skill.76- Image and avatar wrappers.7778Read the barrel before writing a new primitive. If what you need is genuinely79missing and is presentation-only with no module imports, add it to `theme/ui`80and export it from the barrel — do not fork a local copy inside a module.8182Long lists use `FlatList`, never `ScrollView` + `.map()`.8384## i18n + accessibility procedure85861. Add keys to the i18n catalog under the feature's namespace.872. Read them with `useTranslation` and pass translations to a11y props too —88 accessibility copy is user-facing copy.8990```tsx91const { t } = useTranslation();92const selectLabel = t('settings.themePicker.selectTheme');93const label = `${selectLabel} ${displayName}`;9495return (96 <Touchable97 accessible98 button99 accessibilityHint={selectLabel}100 accessibilityLabel={label}101 accessibilityRole="button"102 accessibilityState={{ selected }}103 style={styles.container}104 onPress={handlePress}>105```106107Note the JSX prop order lint enforces: `key` first, then shorthand booleans,108then regular props alphabetically, then callbacks last.109110## Toast111112Import the app-facing `showToast` adapter (conventionally `utils/toast`), never113the visual layer under `theme/ui`. The adapter carries defaults and typically114queues toasts fired before the provider mounts; importing the runtime layer115directly loses both.116117## Worklets and gestures118119Call JS from a worklet with `scheduleOnRN` from `react-native-worklets`:120121```tsx122.onEnd(event => {123 'worklet';124125 const shouldDismiss =126 event.translationY > dismissThreshold ||127 event.velocityY > dismissVelocityThreshold;128129 if (shouldDismiss) {130 scheduleOnRN(onDismiss);131 }132})133```134135Never import `runOnJS` from `react-native-reanimated` — it races under the136worklets runtime these apps use. Values read inside worklets must be137`useSharedValue`, not `useRef`. Mark worklet callbacks with the `'worklet'`138directive.139140## Where a component belongs141142- Reusable, presentation-only, no module or Redux imports → the shared143 `theme/ui` kit, exported from its barrel.144- Feature-specific → inside its module: `components/` for small pieces,145 `fragments/` for larger screen sections, `screens/` for route-level146 orchestration.147- Import other modules only via their barrel (`modules/<name>`). ESLint148 `no-restricted-imports` blocks internals and `madge` catches the cycles.149150See `add-feature` for the full module anatomy. The layer rule: screens151orchestrate, fragments section, components present, hooks own logic. A component152that reaches for `useAppDispatch` or `useAppSelector` has taken on a job that153belongs to its screen's hook.154155## Danger zone — never do156157- Never `import { StyleSheet } from 'react-native'` — themes break silently.158- Never write a hex/rgb color in a component; colors live only in the theme file.159- Never use inline styles or single-element style arrays.160- Never put raw text outside the kit's `Text` component.161- Never hardcode user-facing strings, including alerts and a11y copy.162- Never import the toast visual layer from feature code — use the adapter.163- Never use `runOnJS`; use `scheduleOnRN`.164- Never call `setState` synchronously in a `useEffect` body — initialize165 `useState` with the right value instead.166- Never invent a theme token or i18n namespace you did not read from the repo.167168## Definition of done169170- [ ] Lint passes (run the `--fix` variant first for import/style order).171- [ ] Typecheck passes.172- [ ] New copy is in the i18n catalog; a11y labels/hints/roles present.173- [ ] Styles use real theme tokens; no color literals or inline styles.174- [ ] Imports go through module barrels; run `madge` if module imports changed.175- [ ] Long lists use `FlatList`.176- [ ] Optional: `verify` for runtime behavior, `gitmoji` if committing.