@alex8203/rn-components
v0.2.0
Published
A lightweight, native React Native component library with dark/light theme support
Maintainers
Readme
rn-components
A lightweight React Native component library with dark/light theme support, built with TypeScript strict mode and native platform best practices.
- ✅ iOS & Android native feel (Pressable, android_ripple, Platform.select)
- ✅ Dark / Light / System theme via
ThemeProvider - ✅ Optional react-hook-form integration
- ✅ Fully typed —
strict: true, zeroany - ✅ Tree-shakeable — CJS + ESM builds
Installation
npm install @alex8203/rn-components
# or
pnpm add @alex8203/rn-components
# or
bun add @alex8203/rn-componentsPeer dependencies
npm install react react-native
# react-hook-form is optional
npm install react-hook-formSetup
Wrap your app with ThemeProvider. That's it.
import { ThemeProvider } from 'rn-components';
export default function App() {
return (
<ThemeProvider mode="system">
{/* your app */}
</ThemeProvider>
);
}| mode | Description |
|--------|-------------|
| "system" | Follows device dark/light setting (default) |
| "light" | Always light |
| "dark" | Always dark |
Custom theme
import { ThemeProvider } from 'rn-components';
import type { PartialTheme } from 'rn-components';
const myTheme: PartialTheme = {
colors: {
primary: '#6366F1',
secondary: '#8B5CF6',
},
radii: {
md: 12,
},
};
<ThemeProvider mode="system" theme={myTheme}>
{/* your app */}
</ThemeProvider>useTheme
import { useTheme } from 'rn-components';
function MyComponent() {
const { theme, mode } = useTheme();
return <View style={{ backgroundColor: theme.colors.background }} />;
}Components
Button
import { Button } from 'rn-components';
<Button label="Save" variant="primary" onPress={handleSave} />
<Button label="Delete" variant="destructive" />
<Button label="Loading..." loading />
<Button label="Full width" fullWidth />| Prop | Type | Default |
|------|------|---------|
| label | string | required |
| variant | primary \| secondary \| outline \| ghost \| destructive | primary |
| size | sm \| md \| lg | md |
| loading | boolean | false |
| disabled | boolean | false |
| fullWidth | boolean | false |
| leftIcon | ReactNode | — |
| rightIcon | ReactNode | — |
IconButton
import { IconButton } from 'rn-components';
import { Ionicons } from '@expo/vector-icons';
<IconButton
icon={<Ionicons name="heart" size={20} color="#fff" />}
accessibilityLabel="Like"
variant="primary"
/>| Prop | Type | Default |
|------|------|---------|
| icon | ReactNode | required |
| accessibilityLabel | string | required |
| variant | same as Button | primary |
| size | sm \| md \| lg | md |
Text
import { Text } from 'rn-components';
<Text variant="h1">Heading 1</Text>
<Text variant="body" color="#666">Paragraph</Text>| variant | Usage |
|-----------|-------|
| h1 h2 h3 | Headings |
| body bodySmall | Paragraphs |
| label | Form labels |
| caption | Helper / hint text |
| overline | Section labels (uppercase) |
TextInput
import { TextInput } from 'rn-components';
<TextInput
label="Email"
placeholder="[email protected]"
value={email}
onChangeText={setEmail}
helperText="We'll never share your email"
keyboardType="email-address"
autoCapitalize="none"
/>
// With error
<TextInput
label="Username"
value={username}
onChangeText={setUsername}
errorMessage="Username already taken"
/>With react-hook-form
import { useForm } from 'react-hook-form';
import { TextInput } from 'rn-components';
const { control } = useForm();
<TextInput
label="Email"
name="email"
control={control}
rules={{ required: 'Required', pattern: { value: /\S+@\S+/, message: 'Invalid email' } }}
/>PasswordInput
import { PasswordInput } from 'rn-components';
// Built-in eye icon
<PasswordInput label="Password" value={password} onChangeText={setPassword} />
// Custom icon (e.g. Ionicons)
<PasswordInput
label="Password"
value={password}
onChangeText={setPassword}
renderEyeIcon={({ visible, color }) => (
<Ionicons name={visible ? 'eye' : 'eye-off'} size={20} color={color} />
)}
/>Checkbox
import { Checkbox } from 'rn-components';
<Checkbox
state={checked}
label="Accept terms and conditions"
onChange={(isChecked) => setChecked(isChecked ? 'checked' : 'unchecked')}
/>| state | checked \| unchecked \| indeterminate |
|---------|-----------------------------------------|
Switch
import { Switch } from 'rn-components';
<Switch label="Notifications" value={enabled} onValueChange={setEnabled} />Uses React Native's native Switch — looks different on iOS/Android intentionally. For react-hook-form, wrap with
<Controller>.
Select
import { Select } from 'rn-components';
<Select
label="Country"
placeholder="Select a country"
value={country}
onValueChange={setCountry}
options={[
{ label: 'Cuba', value: 'cu' },
{ label: 'Mexico', value: 'mx' },
{ label: 'Spain', value: 'es' },
]}
/>SearchSelect
Like Select but with a search input to filter options.
import { SearchSelect } from 'rn-components';
import { Ionicons } from '@expo/vector-icons';
<SearchSelect
label="Country"
options={countries}
value={country}
onValueChange={setCountry}
searchIcon={<Ionicons name="search" size={16} color="#999" style={{ marginRight: 8 }} />}
clearIcon={<Ionicons name="close-circle" size={16} color="#999" />}
/>OTPInput
import { OTPInput } from 'rn-components';
<OTPInput
length={6}
value={code}
onChangeText={setCode}
onComplete={(value) => verifyCode(value)}
/>Avatar
import { Avatar } from 'rn-components';
// Image with fallback to initials
<Avatar uri="https://example.com/photo.jpg" name="John Doe" size="md" />
// Initials only
<Avatar name="Ana García" size="lg" backgroundColor="#5856D6" />| size | Dimensions |
|--------|-----------|
| sm | 32px |
| md | 40px |
| lg | 56px |
| xl | 72px |
Badge
import { Badge } from 'rn-components';
<Badge label="New" variant="success" />
<Badge label="3" variant="error" size="sm" />| variant | info \| success \| warning \| error \| neutral |
Card
import { Card } from 'rn-components';
<Card variant="elevated">
<Text variant="label">Title</Text>
<Text variant="caption">Subtitle</Text>
</Card>| variant | Description |
|-----------|-------------|
| elevated | Shadow (iOS) / elevation (Android) |
| outlined | Border, no shadow |
| filled | Surface background |
Divider
import { Divider } from 'rn-components';
<Divider />
<Divider orientation="vertical" style={{ height: 24 }} />Spinner
import { Spinner } from 'rn-components';
<Spinner size="md" />
<Spinner size="lg" color="#34C759" />Modal
import { Modal } from 'rn-components';
<Modal visible={open} onClose={() => setOpen(false)}>
<Text variant="h3">Confirm action</Text>
<Button label="Confirm" onPress={handleConfirm} />
</Modal>Toast
Wrap your app with ToastProvider and use the useToast hook anywhere.
// App.tsx
import { ToastProvider } from 'rn-components';
<ThemeProvider mode="system">
<ToastProvider>
<App />
</ToastProvider>
</ThemeProvider>// Any component
import { useToast } from 'rn-components';
function MyComponent() {
const { show } = useToast();
return (
<Button
label="Save"
onPress={() => show({ message: 'Saved!', variant: 'success' })}
/>
);
}| Option | Type | Default |
|--------|------|---------|
| message | string | required |
| variant | info \| success \| warning \| error | info |
| position | top \| bottom | bottom |
| duration | number (ms) | 3000 |
SafeAreaView
import { SafeAreaView } from 'rn-components';
<SafeAreaView>
{/* content */}
</SafeAreaView>Handles safe area insets on both iOS and Android automatically.
KeyboardAvoidingView
import { KeyboardAvoidingView } from 'rn-components';
<SafeAreaView>
<KeyboardAvoidingView>
<ScrollView>
<TextInput label="Email" ... />
<PasswordInput label="Password" ... />
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>Uses behavior="padding" on iOS and behavior="height" on Android automatically.
react-hook-form
TextInput, PasswordInput, and Checkbox have built-in RHF support via name + control props.
For Switch and Select, use RHF's <Controller> directly:
import { Controller, useForm } from 'react-hook-form';
import { Switch, Select } from 'rn-components';
const { control } = useForm();
<Controller
name="notifications"
control={control}
render={({ field: { value, onChange } }) => (
<Switch label="Notifications" value={value} onValueChange={onChange} />
)}
/>
<Controller
name="country"
control={control}
render={({ field: { value, onChange } }) => (
<Select options={countries} value={value} onValueChange={onChange} />
)}
/>License
MIT
