@gj-kit/expo-ui
React Native and web primitives: an unnamed IconButton, Tabs, or Slider is a compile error.
- npm
0.8.5 - 0 runtime dependencies
- 332 @ts-expect-error guards
- 850+ unit tests
- 58 components, 31 color roles
Keep learning
Section titled “Keep learning”Why this exists
Section titled “Why this exists”In a React Native design system the failures are silent: an IconButton ships with no accessibility label, a Tabs value is typo’d so the panel renders blank, an EmptyState action renders a button whose onPress was never wired, and a hand-assembled theme object leaks undefined into a style prop. None of that fails the build — it fails on a screen reader, in production, on someone else’s device.
What it does about it
Section titled “What it does about it”- Measured Android popup coordinates — Android anchored Menu and Select measure root-relative trigger page coordinates to match the full-window Modal. Status-bar insets are not hardcoded; iOS and web retain their existing placement paths.
- Accessible names the type demands — IconButton without accessibilityLabel, a rich-children Button with no name, and a range Slider given one label instead of a two-thumb tuple are all rejected.
- Dead buttons do not compile — ButtonInteractionProps is a union: onPress is required unless disabled or loading is literally true, and an EmptyState action must carry both label and onPress.
- Tabs cannot lose a panel — panels is typed
Readonly<Record<NoInfer<ItemValue>, NonNullable<ReactNode>>>and value is NoInfer-wrapped, so a typo’d value, a missing panel, and a null panel all fail typecheck. - createTheme is the only door — UiProvider’s theme prop accepts only a branded Theme or ThemePair produced by createTheme or createThemes; a hand-assembled token object is a compile error.
- No design literals in the source — A guard test walks every .tsx file under src/components recursively and fails on any quoted hex color, any numeric fontSize, or any quoted numeric fontWeight.
Golden path
Section titled “Golden path”Outcome: A themed, accessible button rendered from one application-wide provider.
1. Install
Section titled “1. Install”pnpm add @gj-kit/expo-ui2. Keep the app-owned boundary explicit
Section titled “2. Keep the app-owned boundary explicit”Create themes once and mount UiProvider at the component that wraps your app. Native Button, IconButton, Menu and Select own their press feedback through native styles; their default interactions do not require NativeWind.
3. Start with the smallest integration
Section titled “3. Start with the smallest integration”Copy this first, then replace only the app-owned values named above.
import { Button, UiProvider, enStrings } from '@gj-kit/expo-ui';import { createThemes } from '@gj-kit/expo-ui/theme';
const themes = createThemes();
export function App() { return ( <UiProvider theme={themes} strings={enStrings}> <Button label="Get started" onPress={() => {}} /> </UiProvider> );}What that looks like
Section titled “What that looks like”Tabs requires the tablist’s accessible name and one non-null panel per item value, so a forgotten panel stops the build instead of rendering blank.
import { Tabs, Text, UiProvider, createThemes } from '@gj-kit/expo-ui';
declare const onChange: (value: 'overview' | 'history') => void; // the app owns tab stateconst items = [{ label: 'Overview', value: 'overview' }, { label: 'History', value: 'history' }] as const;
export const ProfileTabs = () => ( <UiProvider theme={createThemes({ light: { colors: { primary: '#1769C2' } } })}> <Tabs accessibilityLabel="Profile sections" items={items} value="overview" onChange={onChange} // Delete the history entry below and tsc stops the build: // TS2741: Property 'history' is missing in type '{ overview: JSX.Element; }' panels={{ overview: <Text>Overview</Text>, history: <Text>History</Text> }} /> </UiProvider>);Use it when
Section titled “Use it when”Use it when one design language, controlled component state, overlays, and accessibility behavior must work across native and web targets.
Do not use it when
Section titled “Do not use it when”Do not use it for product routes, data stores, analytics, or product-specific copy.
Runtime and peers
Section titled “Runtime and peers”Engines: node >=20
| Peer | Supported range | Required for |
|---|---|---|
react |
>=18 |
the package integration |
react-native |
>=0.79 |
the package integration |
react-native-safe-area-context |
>=4 |
documented optional subpaths only |
react-native-web |
>=0.21 |
documented optional subpaths only |
Public subpaths
Section titled “Public subpaths”| Entry | Exported symbols |
|---|---|
Root entry |
361 |
./insets |
11 |
./insets/pure |
6 |
./tailwind |
4 |
./theme |
26 |
Safety boundary
Section titled “Safety boundary”Keep optional safe-area and React Native Web peers behind their documented subpaths. Supply application copy through strings rather than baking product copy into primitives.
API reference
Section titled “API reference”complete API reference contains every public function, class, type, constant, and error contract generated from release declarations. Each item shows its import path, release signature, and package version.
Package relationships
Section titled “Package relationships”| Relationship | Package | Why combine them |
|---|---|---|
| Standalone | — | This package can be adopted without another GJ Kit package. |
| Optional companion | @gj-kit/expo-media |
Render media selection and upload state with accessible primitives. |
| Optional companion | @gj-kit/expo-auth |
Show application-owned session state without coupling either package. |