@kyaw-min-khant/rnc
v2.1.0
Published
shadcn-style React Native components — copied into your project via CLI, not installed as a runtime dependency
Readme
RNC — React Native Components
shadcn-style React Native components with full StyleSheet and NativeWind (Tailwind) support. There's nothing to npm install — a CLI copies each component's source directly into your project, so you own the code and only add what you actually use. Every component is theme-aware — customize colors and roundness once via ThemeProvider and all components update automatically.
Table of Contents
Getting Started
npx @kyaw-min-khant/rnc init
npx @kyaw-min-khant/rnc add buttoninit asks where you want components, the theme, and utilities placed in your project, and whether to use an @/-style import alias instead of relative imports (recommended — say yes and it wires up tsconfig.json for you). It writes those choices to a components.json. add then copies the requested component's source there — along with any shared files it needs (like ThemeContext.tsx) — rewriting its imports to match your setup automatically.
The examples below assume the defaults offered by init:
| Alias | Default path |
|---|---|
| components | src/components/ui |
| theme | src/theme |
| utils | src/lib |
with the import alias enabled (@ → src). If you picked different paths, or opted out of the alias, adjust the import paths in the examples accordingly — with the alias off, the same imports look like ../../theme/ThemeContext (relative), still correctly rewritten either way.
Setup
Wrap your root component with ThemeProvider, exported from the theme file rnc add copies in. You can pass a partial theme — anything you omit falls back to the default.
import { ThemeProvider } from '@/theme/ThemeContext';
export default function App() {
return (
<ThemeProvider
theme={{
colors: {
primary: '#6200ee',
secondary: '#03dac6',
},
roundness: 8,
}}
>
<YourApp />
</ThemeProvider>
);
}CLI Reference
rnc init
Creates components.json in the current directory. Prompts for three paths (components, theme, utils), and whether to use an import alias:
- Yes (recommended) — prompts for an alias prefix (default
@) and the directory it should point to (defaultsrc), writes@/*→src/*intocompilerOptions.pathsintsconfig.json(creating a minimal one if you don't have one), and prints thebabel-plugin-module-resolversnippet to add tobabel.config.jsso Metro resolves it at runtime too —tsconfig.jsonalone only helps TypeScript/your editor, not the bundler. - No — components are wired together with plain relative imports (
../../theme/ThemeContext), no extra config needed.
Refuses to overwrite an existing components.json without confirmation.
rnc add
npx @kyaw-min-khant/rnc add <component> [components...]
npx @kyaw-min-khant/rnc add dropdown skeleton --overwriteCopies one or more components into your project. Resolves each component's dependencies automatically (e.g. adding dropdown also adds theme and utils if you don't already have them) and installs any npm packages a component needs via whichever package manager your project uses (detected from package-lock.json / yarn.lock / pnpm-lock.yaml / bun.lockb). Existing files are skipped unless you pass -o, --overwrite.
rnc list
Prints every available component name and a one-line description.
components.json
{
"$schema": "https://kyaw-min-khant.dev/rnc/schema/components.json",
"aliases": {
"components": "src/components/ui",
"theme": "src/theme",
"utils": "src/lib"
},
"importAlias": {
"prefix": "@",
"baseDir": "src"
}
}aliases controls where files are written on disk — point these at wherever fits your project structure. importAlias is optional; omit it (or answer "No" during init) to fall back to relative imports. When present, rnc add rewrites every copied component's internal imports as <prefix>/<path relative to baseDir> (e.g. @/theme/ThemeContext) instead of a relative path — falling back to relative for any dependency that lands outside baseDir.
Theme
RNCTheme interface
| Token | Default | Description |
|---|---|---|
| colors.primary | #6200ee | Main brand color |
| colors.secondary | #03dac6 | Secondary brand color |
| colors.background | #ffffff | Screen background |
| colors.surface | #f5f5f5 | Card / input background |
| colors.error | #b00020 | Error states |
| colors.success | #388e3c | Success states |
| colors.warning | #f57c00 | Warning states |
| colors.onPrimary | #ffffff | Text/icon on primary bg |
| colors.onSecondary | #000000 | Text/icon on secondary bg |
| colors.onSurface | #000000 | Text/icon on surface |
| colors.outline | #6200ee | Borders and outlines |
| roundness | 8 | Border radius for all components |
useTheme()
Access the active theme inside your own components:
import { useTheme } from '@/theme/ThemeContext';
function MyComponent() {
const theme = useTheme();
return <View style={{ backgroundColor: theme.colors.surface }} />;
}Components
Button
npx @kyaw-min-khant/rnc add buttonimport { Button } from '@/components/ui/Button';
<Button title="Submit" onPress={() => {}} />
<Button title="Secondary" onPress={() => {}} variant="secondary" />
<Button title="Outline" onPress={() => {}} variant="outline" />
<Button title="Ghost" onPress={() => {}} variant="ghost" />
<Button title="Loading" onPress={() => {}} loading />
<Button title="Disabled" onPress={() => {}} disabled />| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | — | Button label |
| onPress | () => void | — | Press handler |
| variant | 'primary' \| 'secondary' \| 'outline' \| 'ghost' | 'primary' | Visual style |
| disabled | boolean | false | Disables interaction |
| loading | boolean | false | Shows spinner, disables interaction |
| style | ViewStyle | — | Extra container styles |
| textStyle | TextStyle | — | Extra text styles |
| className | string | — | NativeWind classes |
| textClassName | string | — | NativeWind text classes |
Input
npx @kyaw-min-khant/rnc add inputimport { Input } from '@/components/ui/Input';
<Input label="Email" placeholder="[email protected]" />
<Input label="Filled" variant="filled" placeholder="..." />
<Input label="Underlined" variant="underlined" placeholder="..." />
<Input label="With error" placeholder="..." error="This field is required" />
<Input
label="With icons"
placeholder="Search..."
leftIcon={<SearchIcon />}
rightIcon={<CloseIcon />}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Label above the input |
| error | string | — | Error message below the input |
| variant | 'outlined' \| 'filled' \| 'underlined' | 'outlined' | Border style |
| leftIcon | ReactNode | — | Icon on the left side |
| rightIcon | ReactNode | — | Icon on the right side |
| containerStyle | ViewStyle | — | Outer wrapper styles |
| inputStyle | TextStyle | — | Inner TextInput styles |
| labelStyle | TextStyle | — | Label text styles |
| errorStyle | TextStyle | — | Error text styles |
| className | string | — | NativeWind classes |
| ...TextInputProps | | | All React Native TextInput props are supported |
Badge
npx @kyaw-min-khant/rnc add badgeimport { Badge } from '@/components/ui/Badge';
<Badge label="New" color="primary" />
<Badge label="Error" color="error" variant="outline" />
<Badge count={5} color="error" />
<Badge count={120} maxCount={99} color="primary" />
<Badge label="Large" color="success" size="large" />| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Text to display |
| count | number | — | Numeric count (use instead of label) |
| maxCount | number | 99 | Truncates count above this value (e.g. "99+") |
| variant | 'filled' \| 'outline' | 'filled' | Visual style |
| color | 'primary' \| 'secondary' \| 'error' \| 'success' \| 'warning' | 'primary' | Color palette |
| size | 'small' \| 'medium' \| 'large' | 'medium' | Badge size |
| style | ViewStyle | — | Extra container styles |
| textStyle | TextStyle | — | Extra text styles |
| className | string | — | NativeWind classes |
Dropdown
npx @kyaw-min-khant/rnc add dropdownimport { Dropdown } from '@/components/ui/Dropdown';
const options = [
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
{ label: 'Disabled', value: 'c', disabled: true },
];
<Dropdown
label="Choose one"
placeholder="Select..."
options={options}
value={selected}
onSelect={setSelected}
isSearchable
/>| Prop | Type | Default | Description |
|---|---|---|---|
| options | DropdownOption[] | — | List of { label, value, disabled? } |
| value | T \| null | — | Currently selected value (controlled) |
| onSelect | (value: T) => void | — | Called when an option is selected |
| onChange | (value: T) => void | — | Same as onSelect, provided for API consistency |
| placeholder | string | 'Select...' | Placeholder text |
| placeholderTextColor | string | — | Color of the placeholder and search input placeholder |
| label | string | — | Label above the trigger |
| isSearchable | boolean | true | Shows a search input in the menu |
| disabled | boolean | false | Disables the trigger |
| dropDownPosition | 'top' \| 'bottom' | 'bottom' | Menu open direction |
| dropDownLeftIcon | ReactNode | — | Icon on the left of the trigger |
| dropDownRightIcon | ReactNode | — | Icon on the right of the trigger |
| renderItem | (item, meta) => ReactNode | — | Custom option renderer |
| style | ViewStyle | — | Outer wrapper styles |
| dropDownStyle | ViewStyle | — | Trigger button styles |
| menuStyle | ViewStyle | — | Dropdown menu container styles |
| optionStyle | ViewStyle | — | Individual option row styles |
| optionTextStyle | TextStyle | — | Option label text styles |
| selectedOptionStyle | ViewStyle | — | Styles applied to the selected option row |
| selectedOptionTextStyle | TextStyle | — | Styles applied to the selected option's text |
| labelStyle | TextStyle | — | Label text styles |
| className | string | — | NativeWind classes for the wrapper |
| dropDownClassName | string | — | NativeWind classes for the trigger |
FloatingActionButton
npx @kyaw-min-khant/rnc add floating-action-buttonimport { FloatingActionButton } from '@/components/ui/FloatingActionButton';
// Simple FAB
<FloatingActionButton
icon={<PlusIcon />}
onPress={() => {}}
/>
// Speed dial
<FloatingActionButton
icon={<PlusIcon />}
onPress={() => {}}
actions={[
{ icon: <EditIcon />, label: 'Edit', onPress: () => {} },
{ icon: <ShareIcon />, label: 'Share', onPress: () => {} },
]}
/>
// Extended FAB with label
<FloatingActionButton
icon={<PlusIcon />}
label="Create"
onPress={() => {}}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| onPress | () => void | — | Press handler (or toggle for speed dial) |
| icon | ReactNode | — | Icon inside the FAB |
| label | string | — | Shows an extended FAB with text |
| actions | FABAction[] | [] | Speed-dial actions above the FAB |
| position | 'bottom-right' \| 'bottom-left' \| 'bottom-center' | 'bottom-right' | Screen position |
| size | 'small' \| 'medium' \| 'large' | 'medium' | FAB size |
| color | string | theme.colors.primary | Background color |
| iconColor | string | theme.colors.onPrimary | Icon/label color |
| disabled | boolean | false | Disables the FAB |
| style | ViewStyle | — | Extra styles |
Skeleton
Loading placeholder with a pulsing opacity animation (built on React Native's core Animated API — no extra dependency required). Comes in two shapes.
npx @kyaw-min-khant/rnc add skeletonimport { Skeleton } from '@/components/ui/Skeleton';
// Rectangular skeleton, e.g. a text line
<Skeleton width="60%" height={16} />
// Circle skeleton, e.g. an avatar placeholder
<Skeleton variant="circle" size={48} />| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'rectangular' \| 'circle' | 'rectangular' | Shape of the skeleton |
| width | number \| string | '100%' | Width — number (px) or percentage string like '60%' (rectangular only) |
| height | number \| string | 16 | Height — number (px) or percentage string (rectangular only) |
| size | number | 40 | Diameter in px (circle only) |
| borderRadius | number | theme.roundness | Corner radius (rectangular only) |
| color | string | #9c9c9d | Fill color |
DateTimePicker
A fully custom, inline date & time picker with no external calendar dependency. It renders directly in your layout — no modal or trigger field — and steps through month → day → time.
npx @kyaw-min-khant/rnc add date-time-pickerimport { DateTimePicker } from '@/components/ui/DateTimePicker';
const [date, setDate] = useState(new Date());
// Full date + time (default)
<DateTimePicker value={date} onChange={setDate} />
// Date only
<DateTimePicker value={date} onChange={setDate} mode="date" />
// Time only, 12-hour with AM/PM
<DateTimePicker value={date} onChange={setDate} mode="time" is24Hour={false} />
// Bounded range, 15-minute steps, week starts on Monday
<DateTimePicker
value={date}
onChange={setDate}
minimumDate={new Date()}
maximumDate={new Date(2027, 0, 1)}
minuteInterval={15}
firstDayOfWeek={1}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| value | Date \| null | new Date() | Selected date/time |
| onChange | (date: Date) => void | — | Fires whenever the year, month, day, hour, minute or AM/PM changes |
| mode | 'date' \| 'time' \| 'datetime' | 'datetime' | Which parts of the date are selectable |
| minimumDate | Date | — | Earliest selectable day |
| maximumDate | Date | — | Latest selectable day |
| is24Hour | boolean | true | 24-hour hour wheel instead of 12-hour + AM/PM |
| minuteInterval | 1 \| 5 \| 10 \| 15 \| 30 | 1 | Step between selectable minutes |
| firstDayOfWeek | 0 \| 1 | 0 | Week starts on Sunday (0) or Monday (1) |
| color | string | theme.colors.primary | Accent color for the selected day/month and wheel highlight |
| style | ViewStyle | — | Extra container styles |
| className | string | — | NativeWind classes |
Selecting a day in
"datetime"mode automatically advances to the time step. Tapping the month/year header on the day grid jumps back to the month picker.
Utilities
cn()
Merge class names conditionally for NativeWind. Added automatically alongside any component that needs it (e.g. Dropdown), or on its own:
npx @kyaw-min-khant/rnc add utilsimport { cn } from '@/lib/cn';
<View className={cn('p-4 rounded', isActive && 'bg-blue-500', className)} />License
UNLICENSED — private package by KMK-MOBILE.
