@haykal/ui-native
v1.0.0
Published
React Native design system powered by **Uniwind** (Tailwind CSS for React Native). A publishable, extensible, monorepo-friendly component library.
Readme
@haykal/ui-native
React Native design system powered by Uniwind (Tailwind CSS for React Native). A publishable, extensible, monorepo-friendly component library.
Installation
pnpm add @haykal/ui-native @expo/vector-icons expo-checkbox expo-imageQuick Start
1. Configure Metro in your app
// metro.config.js
const { withUniwindConfig } = require('uniwind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withUniwindConfig(config, {
cssEntryFile: './src/global.css',
dtsFile: './src/uniwind-types.d.ts',
});2. Create global.css
@import 'tailwindcss';
@import 'uniwind';
@source '../../packages/ui-native/src';
@theme {
/* Copy tokens from @haykal/ui-native/src/styles/global.css */
}3. Import CSS first in entry file
import './src/global.css'; // Must be first!4. Use components
import { Button, Checkbox, Icon, Switch } from '@haykal/ui-native';
<Button onPress={() => {}}>Save</Button>
<Switch
label="Push Notifications"
description="Receive important updates."
value={enabled}
onValueChange={setEnabled}
variant="success"
size="lg"
/>
<Checkbox
label="Accept terms"
value={accepted}
onValueChange={setAccepted}
size="sm"
/>
<Icon name="settings-outline" size={20} />Breaking Change (Switch + Checkbox)
checked->valueonCheckedChange->onValueChange- Styled field API added:
label,description,variant, andsize
Exports
| Path | Description |
| -------------------------------- | ------------------------------ |
| @haykal/ui-native | Components + hooks + utils |
| @haykal/ui-native/styles | CSS theme tokens |
| @haykal/ui-native/utils | cn(), cva(), variant types |
| @haykal/ui-native/hooks | useColorScheme, useTheme |
| @haykal/ui-native/primitives | withUniwind wrappers |
| @haykal/ui-native/action-sheet | ActionSheet components & hooks |
| @haykal/ui-native/types | Shared TypeScript types |
Key Components
Switch(nativereact-nativeSwitch with styled labels, variants, and sizes)Checkbox(nativeexpo-checkboxwith styled labels, variants, and sizes)Icon(Ionicons via@expo/vector-icons)Button,Input,Textarea,Card,Avatar,Badge,Tabs,Accordion, and moreActionSheet— iOS-like modal action sheet with imperative + declarative API
ActionSheet
A provider-based action sheet built on @gorhom/bottom-sheet.
Required peer dependencies
pnpm add @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handlerExpo users: install Reanimated via
expo-reanimatedand ensure Reanimated's Babel plugin is in yourbabel.config.js.
Setup
Wrap your app root with GestureHandlerRootView (once) and ActionSheetProvider (once):
// app/_layout.tsx (Expo Router example)
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { ActionSheetProvider } from '@haykal/ui-native';
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ActionSheetProvider>
<Stack />
</ActionSheetProvider>
</GestureHandlerRootView>
);
}Imperative API (recommended)
import { useActionSheet, Button } from '@haykal/ui-native';
function PostActions() {
const { showActionSheet } = useActionSheet();
const onPress = async () => {
const selected = await showActionSheet({
title: 'Post actions',
message: 'Choose an action below',
actions: [
{ key: 'edit', label: 'Edit post' },
{ key: 'archive', label: 'Archive' },
{ key: 'delete', label: 'Delete post', variant: 'destructive' },
{ key: 'cancel', label: 'Cancel', variant: 'cancel' },
],
});
if (selected === 'delete') deletePost();
};
return <Button onPress={onPress}>Actions</Button>;
}Declarative API
import { useState } from 'react';
import { ActionSheet, Button } from '@haykal/ui-native';
function Screen() {
const [open, setOpen] = useState(false);
return (
<>
<Button onPress={() => setOpen(true)}>Open</Button>
<ActionSheet
open={open}
onOpenChange={setOpen}
title="More options"
actions={[
{ key: 'share', label: 'Share' },
{ key: 'report', label: 'Report', variant: 'destructive' },
{ key: 'cancel', label: 'Cancel', variant: 'cancel' },
]}
onSelect={(key) => console.log('Selected:', key)}
/>
</>
);
}Helpers
import {
buildDestructiveConfirmOptions,
buildShareOptions,
createActions,
menuToActionSheet,
} from '@haykal/ui-native';
// Typed action factory
const actions = createActions<'edit' | 'delete' | 'cancel'>([
{ key: 'edit', label: 'Edit' },
{ key: 'delete', label: 'Delete', variant: 'destructive' },
{ key: 'cancel', label: 'Cancel', variant: 'cancel' },
]);
// One-step destructive confirmation
const result = await showActionSheet(
buildDestructiveConfirmOptions({ subject: 'post' }),
);
if (result === 'confirm') deletePost();
// Share-style sheet
const result = await showActionSheet(
buildShareOptions({
actions: [
{ key: 'copy', label: 'Copy link' },
{ key: 'twitter', label: 'Share to Twitter' },
],
}),
);Troubleshooting
| Symptom | Fix |
| --------------------------------------------------------- | ------------------------------------------------------------------------------ |
| "GestureHandlerRootView" not found | Add react-native-gesture-handler dep and wrap the root |
| Animations jank or don't run | Ensure Reanimated babel plugin is in babel.config.js |
| Sheet doesn't appear | Make sure ActionSheetProvider wraps the component calling useActionSheet() |
| "useActionSheet must be inside ActionSheetProvider" error | Move ActionSheetProvider higher in the tree |
Documentation
See IMPLEMENTATION_PLAN.md for architecture details.
