@tradly/react-native-ui
v1.0.37
Published
A reusable, well-structured React Native component library inspired by shadcn/ui
Readme
@tradly/react-native-ui
A reusable, opinionated React Native component library inspired by
shadcn/ui.
TypeScript‑first, themeable, and built from small primitives that are easy to
copy into your own app.
Installation
npm install @tradly/react-native-uiPeer deps (usually already installed in Expo apps):
npm install react react-native react-native-gesture-handler react-native-reanimatedQuick start
1. Wrap your app with UIProvider
UIProvider wires up theming + toasts (and future global contexts) in one
place.
import { UIProvider } from "@tradly/react-native-ui";
export default function App() {
return <UIProvider>{/* your app */}</UIProvider>;
}Optionally:
<UIProvider
defaultTheme={darkTheme} // custom or built-in theme
toastPosition="top" // "top" | "bottom"
>
<App />
</UIProvider>2. Use components
import { Card, Text, Input, Button } from "@tradly/react-native-ui";
function ProfileCard() {
return (
<Card>
<Text variant="h2">Profile</Text>
<Input
label="Name"
placeholder="Your name"
/>
<Button>Save</Button>
</Card>
);
}Theming & tokens
Theme API
import {
ThemeProvider,
useTheme,
lightTheme,
darkTheme,
type Theme,
} from "@tradly/react-native-ui";<ThemeProvider defaultTheme={darkTheme}>
<App />
</ThemeProvider>Create your own theme:
import { ThemeProvider, type Theme } from "@tradly/react-native-ui";
const customTheme: Theme = {
dark: false,
colors: {
background: "#050816",
foreground: "#f9fafb",
card: "#020617",
cardForeground: "#e5e7eb",
primary: "#6366f1",
primaryForeground: "#f9fafb",
secondary: "#111827",
secondaryForeground: "#e5e7eb",
muted: "#1f2937",
mutedForeground: "#9ca3af",
accent: "#22c55e",
accentForeground: "#022c22",
destructive: "#ef4444",
destructiveForeground: "#f9fafb",
border: "#1f2937",
input: "#374151",
ring: "#4f46e5",
},
};
<ThemeProvider defaultTheme={customTheme}>
<App />
</ThemeProvider>;Theme customization (inline)
Use built-ins
<UIProvider defaultTheme={lightTheme}>{/* app */}</UIProvider>
<UIProvider defaultTheme={darkTheme}>{/* app */}</UIProvider>Full custom theme
const myTheme: Theme = {
dark: false,
colors: {
background: "#fafafa",
foreground: "#171717",
card: "#ffffff",
cardForeground: "#171717",
primary: "#10b981", // brand color
primaryForeground: "#ffffff",
secondary: "#f5f5f5",
secondaryForeground: "#171717",
muted: "#f9fafb",
mutedForeground: "#737373",
accent: "#10b981",
accentForeground: "#ffffff",
destructive: "#ef4444",
destructiveForeground: "#ffffff",
border: "#e5e5e5",
input: "#e5e5e5",
ring: "#10b981",
},
};
<UIProvider defaultTheme={myTheme}>{/* app */}</UIProvider>;Extend built-in theme (override a few colors)
const customLight: Theme = {
...lightTheme,
colors: {
...lightTheme.colors,
primary: "#8b5cf6",
accent: "#8b5cf6",
ring: "#8b5cf6",
},
};Helper to generate themes
function createTheme(primaryColor: string, isDark = false): Theme {
return {
dark: isDark,
colors: {
background: isDark ? "#0a0a0a" : "#fafafa",
foreground: isDark ? "#fafafa" : "#171717",
card: isDark ? "#1a1a1a" : "#ffffff",
cardForeground: isDark ? "#fafafa" : "#171717",
primary: primaryColor,
primaryForeground: "#ffffff",
secondary: isDark ? "#262626" : "#f5f5f5",
secondaryForeground: isDark ? "#fafafa" : "#171717",
muted: isDark ? "#1f1f1f" : "#f9fafb",
mutedForeground: isDark ? "#a3a3a3" : "#737373",
accent: primaryColor,
accentForeground: "#ffffff",
destructive: "#ef4444",
destructiveForeground: "#ffffff",
border: isDark ? "#2a2a2a" : "#e5e5e5",
input: isDark ? "#2a2a2a" : "#e5e5e5",
ring: primaryColor,
},
};
}Dynamic switching
function ThemeToggle() {
const { theme, toggleTheme, setTheme } = useTheme();
return (
<View>
<Button
onPress={toggleTheme}
title={`Switch to ${
theme.dark ? "Light" : "Dark"
} mode`}
/>
<Button
onPress={() => setTheme(customLight)}
title="Use custom light"
/>
</View>
);
}Color palette reference (semantic names)
background,foreground,card,cardForegroundprimary,primaryForeground(buttons/links/toggles)secondary,secondaryForegroundmuted,mutedForeground(disabled/subtle)accent,accentForeground(highlights)destructive,destructiveForegroundborder,input,ring
All components automatically read from useTheme()—once you set a theme in
UIProvider, colors propagate everywhere.
📖 For detailed theme customization guide, see THEME_CUSTOMIZATION.md
Access the current theme:
import { useTheme } from "@tradly/react-native-ui";
function ThemedView() {
const { theme, toggleTheme } = useTheme();
return (
<View
style={{
flex: 1,
backgroundColor: theme.colors.background,
}}
>
<Button onPress={toggleTheme}>
Switch to {theme.dark ? "Light" : "Dark"} mode
</Button>
</View>
);
}Design tokens
import { spacing, radii, typography, shadows } from "@tradly/react-native-ui";
const styles = StyleSheet.create({
card: {
padding: spacing.md,
borderRadius: radii.lg,
...shadows.md,
},
title: {
fontSize: typography.fontSizes.xl,
fontWeight: typography.fontWeights.bold,
},
});Typography tokens include:
typography.fontSizes–xs→5xltypography.fontWeights–normal | medium | semibold | boldtypography.lineHeights–tight | normal | relaxedtypography.letterSpacings–tight | normal | widetypography.fonts–sans | heading | mono
Components
Typography (Text)
Semantic text component with shadcn-style variants.
import { Text } from "@tradly/react-native-ui";
<Text variant="h1">Taxing Laughter: The Joke Tax Chronicles</Text>
<Text variant="h2">The People of the Kingdom</Text>
<Text variant="p">
This page shows styles for headings, paragraphs, lists, etc.
</Text>
<Text variant="muted">Muted description text</Text>
<Text variant="inlineCode">const value = "inline code"</Text>Variants
- Headings:
h1 | h2 | h3 | h4 - Body:
p | lead | muted | small | large - Special:
inlineCode - Legacy aliases (still work):
heading | subheading | body | caption | xl
Font families and letter spacing are wired to typography.fonts and
typography.letterSpacings.
Button
Shadcn-inspired button with variants, sizes, loading state, and asChild.
import { Button } from "@tradly/react-native-ui";
// Primary
<Button onPress={save}>Save</Button>
// Variants
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Learn more</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="lg">Large CTA</Button>
<Button size="icon" variant="ghost">{">"}</Button>
// Loading
<Button loading>Please wait</Button>
// asChild – compose with your own Pressable
<Button asChild variant="outline">
<Pressable>
<Text>New Branch</Text>
</Pressable>
</Button>Props
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"size?: "default" | "sm" | "lg" | "icon"disabled?: booleanloading?: booleanasChild?: booleanonPress?: () => voidstyle?: ViewStyle | ViewStyle[]textStyle?: TextStyle | TextStyle[]
Input
Labelled TextInput with error text and high-level variants.
import { Input } from "@tradly/react-native-ui";
// Basic
<Input label="Name" placeholder="Your name" />
// Email
<Input
label="Email"
variant="email"
value={email}
onChange={(v) => setEmail(v)}
/>;
// Password
<Input
label="Password"
variant="password"
error={!password ? "Password is required" : undefined}
/>;Props
- Extends React Native
TextInputProps(exceptonChange) label?: stringerror?: stringcontainerStyle?: ViewStyle | ViewStyle[]inputStyle?: TextStyle | TextStyle[]onChange?(value: string): void– convenience wrapper aroundonChangeTextvariant?: "default" | "email" | "password" | "number" | "search" | "tel" | "url"
The variant sets keyboardType, secureTextEntry, autoCapitalize,
autoCorrect with sensible defaults, but you can still override them via normal
props.
Textarea
Multiline text input component for longer content. Supports labels, descriptions, character limits, and validation.
import { Textarea } from "@tradly/react-native-ui";
// Basic
<Textarea
label="Your message"
placeholder="Type your message here."
rows={4}
/>
// With description/helper text
<Textarea
label="Your message"
placeholder="Type your message here."
description="Your message will be copied to the support team."
rows={4}
/>
// With character limit and counter
<Textarea
label="Bio"
placeholder="Tell us a bit about yourself."
description="You can @mention other users and organizations."
maxLength={500}
showCharCount
rows={6}
/>
// With error state
<Textarea
label="Feedback"
placeholder="Share your thoughts..."
error="Feedback is required"
required
rows={5}
/>
// Controlled
<Textarea
label="Message"
value={message}
onChange={setMessage}
maxLength={1000}
showCharCount
rows={8}
/>Props
- Extends React Native
TextInputProps(exceptonChange,multilineis always true) label?: string– Label textdescription?: string– Helper/description text below textareaerror?: string– Error messagerequired?: boolean– Mark field as required (adds asterisk to label)rows?: number– Number of rows (affects initial height, default: 4)maxLength?: number– Maximum character lengthshowCharCount?: boolean– Show character countercontainerStyle?: ViewStyle | ViewStyle[]textareaStyle?: TextStyle | TextStyle[]onChange?(value: string): void– Convenience wrapper aroundonChangeText
Features
- ✅ Multiline text input (always enabled)
- ✅ Label support with required indicator
- ✅ Description/helper text
- ✅ Character limit with visual feedback
- ✅ Character counter (shows current/max or current count)
- ✅ Error state styling
- ✅ Controlled and uncontrolled modes
- ✅ Customizable initial height via
rowsprop - ✅ Theme-aware styling
- ✅ Text alignment at top (Android)
Best Practices
- Set appropriate
rowsbased on expected content (3 for comments, 6-8 for feedback, 10+ for content creation) - Show character limits before users hit them with
showCharCount - Use
descriptionfor helpful context or instructions - Consider auto-save for longer content to prevent data loss
- Use consistent heights to avoid layout shifts
ColorPicker
A comprehensive color picker component with visual color selection, RGB sliders, hex input, and preset colors. Built entirely with React Native components—no external dependencies.
import { ColorPicker } from "@tradly/react-native-ui";
// Basic
<ColorPicker
label="Background Color"
value={color}
onChange={setColor}
/>
// With all features
<ColorPicker
label="Theme Color"
value={themeColor}
onChange={setThemeColor}
showPresets
showHexInput
showRGBSliders
/>
// Custom preset colors
<ColorPicker
label="Brand Color"
value={brandColor}
onChange={setBrandColor}
presetColors={["#FF5733", "#33FF57", "#3357FF", "#FF33F5"]}
/>
// Minimal (presets only)
<ColorPicker
value={color}
onChange={setColor}
showPresets
showHexInput={false}
showRGBSliders={false}
/>Props
value?: string– Controlled color value (hex string, e.g., "#FF5733")defaultValue?: string– Uncontrolled default color (default: "#000000")onChange?: (color: string) => void– Callback when color changeslabel?: string– Label textdisabled?: boolean– Disabled stateshowPresets?: boolean– Show preset colors (default: true)presetColors?: string[]– Custom preset colors (default: 20 common colors)showHexInput?: boolean– Show hex input field (default: true)showRGBSliders?: boolean– Show RGB sliders and color grid (default: true)style?: ViewStyle | ViewStyle[]testID?: string
Features
- ✅ Visual color picker with HSL color grid
- ✅ Hue slider for color selection
- ✅ RGB sliders (Red, Green, Blue)
- ✅ Hex color input with validation
- ✅ Preset color palette (20 default colors)
- ✅ Custom preset colors support
- ✅ Real-time color preview
- ✅ Controlled and uncontrolled modes
- ✅ Modal-based picker interface
- ✅ Theme-aware styling
- ✅ No external dependencies (pure React Native)
Color Utilities
The component includes built-in color conversion utilities:
hexToRgb(hex: string): RGB | null– Convert hex to RGBrgbToHex(r: number, g: number, b: number): string– Convert RGB to hexrgbToHsl(r: number, g: number, b: number): HSL– Convert RGB to HSLhslToRgb(h: number, s: number, l: number): RGB– Convert HSL to RGBisValidHex(hex: string): boolean– Validate hex colornormalizeHex(hex: string): string– Normalize hex format
Best Practices
- Use preset colors for common brand colors or theme colors
- Show hex input for precise color selection
- Use RGB sliders for fine-tuning colors
- Consider disabling certain features based on use case (e.g., presets-only for quick selection)
- Store selected colors in your app state or theme configuration
MultiInput
A multi-value input component (tag/chip input) that allows users to enter multiple values separated by commas or newlines. Values are displayed as removable tags/chips.
import { MultiInput } from "@tradly/react-native-ui";
// Basic
<MultiInput
label="Tags"
placeholder="Add tags..."
value={tags}
onChange={setTags}
/>
// With validation
<MultiInput
label="Email Addresses"
placeholder="Add email addresses"
value={emails}
onChange={setEmails}
validateValue={(val) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(val) || "Invalid email address";
}}
transformValue={(val) => val.trim().toLowerCase()}
/>
// With max values
<MultiInput
label="Skills"
placeholder="Add up to 5 skills"
value={skills}
onChange={setSkills}
maxValues={5}
/>
// Custom separators
<MultiInput
label="Keywords"
placeholder="Separate with semicolon"
value={keywords}
onChange={setKeywords}
separators={[";", "\n"]}
/>
// Controlled with error
<MultiInput
label="Categories"
value={categories}
onChange={setCategories}
error={categories.length === 0 ? "At least one category required" : undefined}
required
/>Props
value?: string[]– Controlled values arraydefaultValue?: string[]– Uncontrolled default values (default:[])onChange?: (values: string[]) => void– Callback when values changelabel?: string– Label textplaceholder?: string– Placeholder text (default: "Type and press enter or comma")error?: string– Error messagerequired?: boolean– Mark field as requireddisabled?: boolean– Disabled statemaxValues?: number– Maximum number of values allowedseparators?: string[]– Separator characters that trigger tag creation (default:[",", "\n"])validateValue?: (value: string) => boolean | string– Validate each value before adding (returnfalseor error message string to reject)transformValue?: (value: string) => string– Transform value before adding (e.g., trim, lowercase)containerStyle?: ViewStyle | ViewStyle[]inputStyle?: TextStyle | TextStyle[]tagStyle?: ViewStyle | ViewStyle[]– Style for individual tags/chipstagTextStyle?: TextStyle | TextStyle[]– Style for tag texttestID?: string
Features
- ✅ Multiple value input with tag/chip display
- ✅ Automatic tag creation on comma or enter
- ✅ Custom separator characters
- ✅ Remove tags by clicking × button
- ✅ Remove last tag with backspace on empty input
- ✅ Value validation before adding
- ✅ Value transformation (trim, lowercase, etc.)
- ✅ Duplicate detection
- ✅ Maximum values limit
- ✅ Error state styling
- ✅ Helper text (shows count when maxValues is set)
- ✅ Controlled and uncontrolled modes
- ✅ Label support with required indicator
- ✅ Theme-aware styling
- ✅ Customizable tag styles
Usage Patterns
Email addresses:
<MultiInput
label="Recipients"
placeholder="Add email addresses"
validateValue={(val) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(val) || "Invalid email";
}}
transformValue={(val) => val.trim().toLowerCase()}
/>Tags with max limit:
<MultiInput
label="Tags"
maxValues={10}
placeholder="Add tags (max 10)"
/>Custom separator:
<MultiInput
label="Items"
separators={[";"]}
placeholder="Separate items with semicolon"
/>Best Practices
- Use
validateValueto ensure data quality (email format, URL format, etc.) - Use
transformValueto normalize values (trim whitespace, lowercase, etc.) - Set
maxValuesto prevent excessive input - Provide clear placeholder text explaining the expected format
- Use appropriate separators based on your use case (comma for tags, semicolon for lists, etc.)
FileUpload
A flexible file upload component that separates UI from upload logic. You provide the file picker and uploader functions (from any library), and the component handles the UI, previews, progress, and file management.
import {
FileUpload,
FileItem,
FilePickerFn,
FileUploaderFn,
} from "@tradly/react-native-ui";
import * as ImagePicker from "expo-image-picker";
import * as DocumentPicker from "expo-document-picker";
// Example with expo-image-picker
const pickImage: FilePickerFn = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsMultipleSelection: true,
});
if (!result.canceled) {
return result.assets.map((asset) => ({
uri: asset.uri,
name: asset.fileName || `image_${Date.now()}.jpg`,
type: asset.type || "image/jpeg",
size: asset.fileSize,
}));
}
return null;
};
const uploadImage: FileUploaderFn = async (file, onProgress) => {
const formData = new FormData();
formData.append("file", {
uri: file.uri,
type: file.type,
name: file.name,
} as any);
const response = await fetch("https://api.example.com/upload", {
method: "POST",
body: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
if (!response.ok) throw new Error("Upload failed");
const data = await response.json();
return { url: data.url };
};
// Usage - Grid layout (default - gallery style)
<FileUpload
label="Upload Images"
picker={pickImage}
uploader={uploadImage}
accept="image"
multiple
maxFiles={5}
maxSize={5 * 1024 * 1024} // 5MB
showProgress
autoUpload
layout="grid" // Gallery/grid layout (default)
/>
// List layout
<FileUpload
label="Documents"
picker={pickDocument}
layout="list" // List layout with full details
accept="file"
/>
// With custom icons
<FileUpload
label="Media Files"
picker={pickMedia}
icons={{
image: require('./assets/image-icon.png'),
video: <VideoIcon />, // SVG component
audio: "🎵", // Emoji
file: require('./assets/file-icon.png'),
}}
buttonIcon={require('./assets/upload-icon.png')}
/>Props
files?: FileItem[]– Controlled file listonFilesChange?: (files: FileItem[]) => void– Callback when files changepicker: FilePickerFn– Required function that opens file picker (you provide from your chosen library)uploader?: FileUploaderFn– Optional function that uploads a file (you provide)remover?: FileRemoverFn– Optional function that removes a file from serverlabel?: string– Label textaccept?: "image" | "video" | "file" | "all"– Accept file types (default: "all")multiple?: boolean– Allow multiple files (default: false)maxFiles?: number– Maximum number of filesmaxSize?: number– Maximum file size in bytesshowProgress?: boolean– Show upload progress (default: true)autoUpload?: boolean– Auto upload after selection (default: false)disabled?: boolean– Disabled stateerror?: string– Error messagerequired?: boolean– Required fieldtransformPickerResult?: (result: PickerResult) => FileItem– Transform picker result to FileItemlayout?: "grid" | "list"– Display layout (default: "grid" - gallery style)icons?: { image?, video?, audio?, file? }– Custom icons for file types (ReactElement, Image source, or emoji string)buttonIcon?: ReactElement | ImageSourcePropType | string– Custom icon for upload buttoncontainerStyle,buttonStyle,fileItemStyle– Custom styles
FileItem Interface
interface FileItem {
id: string; // Unique identifier
name: string; // File name
uri: string; // File URI/path (for preview)
type?: string; // MIME type
size?: number; // Size in bytes
status?: "pending" | "uploading" | "success" | "error";
progress?: number; // Upload progress (0-100)
error?: string; // Error message
remoteUrl?: string; // Remote URL after upload
metadata?: Record<string, any>;
}Function Signatures
// File picker function
type FilePickerFn = () => Promise<PickerResult | PickerResult[] | null>;
// File uploader function
type FileUploaderFn = (
file: FileItem,
onProgress?: (progress: number) => void
) => Promise<{ url?: string; [key: string]: any }>;
// File remover function
type FileRemoverFn = (file: FileItem) => Promise<void>;Integration Examples
With react-native-image-picker:
import {
launchImageLibrary,
ImagePickerResponse,
} from "react-native-image-picker";
const pickImage: FilePickerFn = () => {
return new Promise((resolve) => {
launchImageLibrary(
{ mediaType: "photo", selectionLimit: 5 },
(response: ImagePickerResponse) => {
if (response.assets) {
resolve(
response.assets.map((asset) => ({
uri: asset.uri!,
name:
asset.fileName ||
`image_${Date.now()}.jpg`,
type: asset.type,
size: asset.fileSize,
}))
);
} else {
resolve(null);
}
}
);
});
};With expo-document-picker:
import * as DocumentPicker from "expo-document-picker";
const pickDocument: FilePickerFn = async () => {
const result = await DocumentPicker.getDocumentAsync({
type: "*/*",
multiple: true,
});
if (!result.canceled) {
return result.assets.map((asset) => ({
uri: asset.uri,
name: asset.name,
type: asset.mimeType,
size: asset.size,
}));
}
return null;
};With custom upload logic:
const uploadToS3: FileUploaderFn = async (file, onProgress) => {
// Your S3 upload logic here
// Call onProgress(percentage) to update progress
// Return { url: "https://..." } on success
};Features
- ✅ Flexible picker integration (works with any picker library)
- ✅ Flexible uploader integration (works with any upload method)
- ✅ Gallery/Grid layout by default - Beautiful grid display of uploaded files
- ✅ List layout option - Detailed list view with progress bars
- ✅ Custom icons support - Use SVG, images, or emojis for file types and button
- ✅ Image/video previews with thumbnails
- ✅ Upload progress tracking
- ✅ File validation (size, type)
- ✅ Error handling and retry
- ✅ Multiple file support
- ✅ File removal
- ✅ Controlled and uncontrolled modes
- ✅ Customizable UI
- ✅ Theme-aware styling
Best Practices
- Provide a
transformPickerResultfunction if your picker returns a different format - Use
validateValuein the transform function for additional validation - Implement proper error handling in your uploader function
- Call
onProgresscallback during upload for real-time progress updates - Use
removerfunction to clean up files from your server when removed from UI
Select
Dropdown select component with search, multi-select, and grouped options support.
import { Select } from "@tradly/react-native-ui";
// Basic single select with label
<Select
label="Fruit"
placeholder="Select a fruit"
options={[
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "orange", label: "Orange" },
]}
value={selected}
onValueChange={setSelected}
/>
// With custom icons
<Select
label="Country"
placeholder="Select a country"
required
icon="🌍" // Custom chevron icon
selectedIcon="✅" // Custom selected item icon
options={countries}
value={country}
onValueChange={setCountry}
/>
// Modal mode (default - centered popup)
<Select
label="Account"
placeholder="Select an account"
options={accounts}
value={selected}
onValueChange={setSelected}
position="modal"
/>
// Dropdown mode (positioned below trigger)
<Select
label="Category"
placeholder="Select a category"
options={categories}
value={selected}
onValueChange={setSelected}
position="dropdown"
/>
// Multi-select
<Select
multiple
placeholder="Select multiple items"
options={options}
value={selectedItems}
onValueChange={setSelectedItems}
/>
// With search
<Select
searchable
searchPlaceholder="Search fruits..."
options={fruits}
value={selected}
onValueChange={setSelected}
/>
// Grouped options
<Select
groups={[
{
label: "North America",
options: [
{ value: "est", label: "Eastern Standard Time (EST)" },
{ value: "cst", label: "Central Standard Time (CST)" },
],
},
{
label: "Europe & Africa",
options: [
{ value: "gmt", label: "Greenwich Mean Time (GMT)" },
{ value: "cet", label: "Central European Time (CET)" },
],
},
]}
value={timezone}
onValueChange={setTimezone}
/>Props
label?: string– Label text (like Input field)value?: string | string[]– controlled value (single: string, multi: string[])defaultValue?: string | string[]– uncontrolled initial valueonValueChange?: (value: string | string[]) => voidplaceholder?: string– default"Select an option"options?: SelectOption[]– flat list of optionsgroups?: SelectGroup[]– grouped options with labelsmultiple?: boolean– enable multi-select modesearchable?: boolean– enable search/filtersearchPlaceholder?: string– default"Search..."disabled?: booleanposition?: "modal" | "dropdown"– Display mode (default:"modal")"modal": Centered popup with backdrop"dropdown": Positioned below trigger (no backdrop)
icon?: ReactElement | ImageSourcePropType | string– Custom icon for trigger chevron (default: ▲/▼)selectedIcon?: ReactElement | ImageSourcePropType | string– Custom icon for selected items (default: ✓)required?: boolean– Mark field as required (adds asterisk to label)triggerStyle?: ViewStyle | ViewStyle[]contentStyle?: ViewStyle | ViewStyle[]itemStyle?: ViewStyle | ViewStyle[]containerStyle?: ViewStyle | ViewStyle[]– Style for label + trigger containertestID?: string
Type definitions
interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}
interface SelectGroup {
label?: string;
options: SelectOption[];
}Subcomponents (for advanced composition)
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectGroup,
SelectLabel,
SelectSeparator,
} from "@tradly/react-native-ui";
// Manual composition
<SelectTrigger onPress={() => setOpen(true)}>
<Text>{selectedLabel || "Select..."}</Text>
</SelectTrigger>
<SelectContent visible={open} onClose={() => setOpen(false)}>
<SelectGroup label="Fruits">
<SelectItem
value="apple"
label="Apple"
selected={selected === "apple"}
onPress={() => setSelected("apple")}
/>
<SelectSeparator />
<SelectItem
value="banana"
label="Banana"
selected={selected === "banana"}
onPress={() => setSelected("banana")}
/>
</SelectGroup>
</SelectContent>Features
- ✅ Label support (like Input field)
- ✅ Two display modes: modal (popup) or dropdown (below trigger)
- ✅ Single and multi-select modes
- ✅ Search/filter functionality (filters both flat and grouped options)
- ✅ Grouped options with labels
- ✅ Controlled and uncontrolled state
- ✅ Disabled state for options and root
- ✅ Modal overlay with backdrop (modal mode)
- ✅ Dropdown positioning below trigger (dropdown mode)
- ✅ Keyboard-friendly (auto-focus search when enabled)
- ✅ "No results" message when search yields empty results
- ✅ Multi-select shows count or single item label in trigger
Calendar
A comprehensive calendar component with date picker and inline modes. Supports single date, multiple dates, and date range selection.
import { Calendar } from "@tradly/react-native-ui";
// Date picker mode (opens in modal)
<Calendar
label="Select Date"
placeholder="Pick a date"
value={date}
onSelect={setDate}
mode="single"
/>
// Inline calendar
<Calendar
displayMode="inline"
mode="single"
value={date}
onSelect={setDate}
/>
// Date range selection
<Calendar
label="Select Date Range"
mode="range"
value={dateRange}
onSelect={setDateRange}
/>
// Multiple dates
<Calendar
label="Select Dates"
mode="multiple"
value={dates}
onSelect={setDates}
/>
// With disabled dates
<Calendar
label="Booking Date"
mode="single"
value={date}
onSelect={setDate}
minDate={new Date()} // Disable past dates
disabledDates={[
{ dayOfWeek: [0, 6] }, // Disable weekends
new Date(2024, 11, 25), // Disable Christmas
]}
/>
// With month/year dropdowns
<Calendar
label="Birth Date"
captionLayout="dropdown" // Month and year dropdowns
mode="single"
value={date}
onSelect={setDate}
/>
// Custom icons
<Calendar
label="Event Date"
icons={{
prevMonth: require('./assets/prev-icon.png'),
nextMonth: require('./assets/next-icon.png'),
}}
mode="single"
value={date}
onSelect={setDate}
/>Props
mode?: "single" | "multiple" | "range"– Selection mode (default: "single")value?: Date | Date[] | DateRange | null– Controlled selected date(s)defaultValue?: Date | Date[] | DateRange | null– Uncontrolled default valueonSelect?: (date: Date | Date[] | DateRange | null) => void– Selection handlerdisabledDates?: DateMatcher | DateMatcher[]– Dates to disableminDate?: Date– Minimum selectable datemaxDate?: Date– Maximum selectable datedisplayMode?: "picker" | "inline"– Display mode (default: "picker")label?: string– Label text (for picker mode)placeholder?: string– Placeholder text (default: "Select a date")required?: boolean– Mark field as requireddisabled?: boolean– Disabled stateerror?: string– Error messagecaptionLayout?: "label" | "dropdown"– Month/year navigation style (default: "label")showWeekNumbers?: boolean– Show week numbers (default: false)showToday?: boolean– Show today indicator (default: true)icons?: { prevMonth?, nextMonth? }– Custom navigation iconscontainerStyle,calendarStyle– Custom stylestestID?: string
DateMatcher Types
// Single date
disabledDates={new Date(2024, 11, 25)}
// Date range
disabledDates={{ from: new Date(2024, 0, 1), to: new Date(2024, 0, 7) }}
// Day of week
disabledDates={{ dayOfWeek: [0, 6] }} // Weekends
// Function
disabledDates={(date) => date < new Date()} // Past datesDateRange Interface
interface DateRange {
from: Date | null;
to: Date | null;
}Features
- ✅ Single date selection
- ✅ Multiple date selection
- ✅ Date range selection with visual feedback
- ✅ Date picker mode (modal) and inline mode
- ✅ Month/year navigation (arrows or dropdowns)
- ✅ Flexible date disabling (single dates, ranges, day of week, functions)
- ✅ Min/max date constraints
- ✅ Today indicator
- ✅ Visual range highlighting
- ✅ Custom icons for navigation
- ✅ Label support with required indicator
- ✅ Error state styling
- ✅ Theme-aware styling
- ✅ Mobile-optimized touch targets
Best Practices
- Use
minDateto disable past dates for booking systems - Use
disabledDateswith{ dayOfWeek: [0, 6] }to disable weekends - Show today clearly with
showToday={true}(default) - Use
captionLayout="dropdown"for easier year navigation - Use range mode for check-in/check-out dates
- Use multiple mode for selecting several dates (e.g., recurring events)
TimePicker
A comprehensive time picker component with visual clock face interface. Supports 12-hour (AM/PM) and 24-hour formats, with both picker and inline display modes.
import { TimePicker } from "@tradly/react-native-ui";
// Date picker mode (opens in modal)
<TimePicker
label="Select Time"
placeholder="Pick a time"
value={time}
onChange={setTime}
format="12h"
/>
// Inline time picker
<TimePicker
displayMode="inline"
format="24h"
value={time}
onChange={setTime}
/>
// 24-hour format
<TimePicker
label="Appointment Time"
format="24h"
value={time}
onChange={setTime}
/>
// With time constraints
<TimePicker
label="Business Hours"
format="12h"
value={time}
onChange={setTime}
minTime={{ hours: 9, minutes: 0 }}
maxTime={{ hours: 17, minutes: 0 }}
/>
// With minute step (5-minute intervals)
<TimePicker
label="Meeting Time"
minuteStep={5}
value={time}
onChange={setTime}
/>
// Custom icon
<TimePicker
label="Alarm Time"
icons={{
clock: require('./assets/clock-icon.png'),
}}
value={time}
onChange={setTime}
/>Props
value?: Time | null– Controlled selected timedefaultValue?: Time | null– Uncontrolled default timeonChange?: (time: Time | null) => void– Time change handlerformat?: "12h" | "24h"– Time format (default: "12h")displayMode?: "picker" | "inline"– Display mode (default: "picker")label?: string– Label text (for picker mode)placeholder?: string– Placeholder text (default: "Select time")required?: boolean– Mark field as requireddisabled?: boolean– Disabled stateerror?: string– Error messageminTime?: Time– Minimum selectable timemaxTime?: Time– Maximum selectable timeminuteStep?: number– Step for minutes (default: 1, e.g., 5 for 5-minute intervals)showSeconds?: boolean– Show seconds picker (default: false)icons?: { clock? }– Custom clock iconcontainerStyle,timePickerStyle– Custom stylestestID?: string
Time Interface
interface Time {
hours: number; // 0-23
minutes: number; // 0-59
}Features
- ✅ Visual clock face interface with hour/minute selection
- ✅ 12-hour format (AM/PM) and 24-hour format
- ✅ Interactive clock hand pointing to selected time
- ✅ Hour and minute mode switching
- ✅ AM/PM toggle for 12-hour format
- ✅ Time constraints (min/max time)
- ✅ Minute step intervals (e.g., 5-minute steps)
- ✅ Date picker mode (modal) and inline mode
- ✅ Large time display in header
- ✅ Custom icons support
- ✅ Label support with required indicator
- ✅ Error state styling
- ✅ Theme-aware styling
- ✅ Mobile-optimized touch targets
Usage Examples
12-hour format with AM/PM:
<TimePicker
label="Wake Up Time"
format="12h"
value={time}
onChange={setTime}
/>24-hour format:
<TimePicker
label="Meeting Time"
format="24h"
value={time}
onChange={setTime}
/>With time constraints:
<TimePicker
label="Business Hours"
minTime={{ hours: 9, minutes: 0 }}
maxTime={{ hours: 17, minutes: 0 }}
value={time}
onChange={setTime}
/>5-minute intervals:
<TimePicker
label="Appointment"
minuteStep={5}
value={time}
onChange={setTime}
/>Best Practices
- Use 12-hour format for user-friendly time selection (AM/PM)
- Use 24-hour format for technical/precise applications
- Set
minTimeandmaxTimefor business hours or appointment scheduling - Use
minuteStep={5}orminuteStep={15}for common appointment intervals - Show time clearly in the header for user confirmation
- Use inline mode for always-visible time selection
- Use picker mode to save screen space
Collapsible
A collapsible component for showing and hiding content with smooth animations. Perfect for FAQ sections, accordions, expandable cards, and progressive disclosure.
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@tradly/react-native-ui";
// Uncontrolled (simple FAQ)
<Collapsible defaultOpen={false}>
<CollapsibleTrigger>
<Text>What is React Native?</Text>
<Text>{isOpen ? "▼" : "▶"}</Text>
</CollapsibleTrigger>
<CollapsibleContent>
<Text>React Native is a framework for building mobile apps...</Text>
</CollapsibleContent>
</Collapsible>
// Controlled (complex state)
const [open, setOpen] = useState(false);
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger>
<Button>Toggle Section</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<View>
<Text>Hidden content here</Text>
</View>
</CollapsibleContent>
</Collapsible>
// With custom trigger (asChild)
<Collapsible>
<CollapsibleTrigger asChild>
<Pressable style={customStyles}>
<Text>Custom Trigger</Text>
</Pressable>
</CollapsibleTrigger>
<CollapsibleContent>
<Text>Content</Text>
</CollapsibleContent>
</Collapsible>
// With Card component
<Card>
<Collapsible>
<CollapsibleTrigger>
<CardHeader>
<CardTitle>Expandable Section</CardTitle>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent>
<Text>This content can be collapsed</Text>
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
// Force mount (always render, just hide)
<Collapsible>
<CollapsibleTrigger>
<Text>Show Advanced Options</Text>
</CollapsibleTrigger>
<CollapsibleContent forceMount>
<Form>
{/* Form fields always mounted */}
</Form>
</CollapsibleContent>
</Collapsible>
// Custom animation duration
<Collapsible>
<CollapsibleTrigger>
<Text>Quick Toggle</Text>
</CollapsibleTrigger>
<CollapsibleContent animationDuration={150}>
<Text>Faster animation</Text>
</CollapsibleContent>
</Collapsible>Components
Collapsible– Root container that manages stateCollapsibleTrigger– Clickable element to toggle open/closedCollapsibleContent– Content that expands/collapses
Props
Collapsible:
open?: boolean– Controlled open statedefaultOpen?: boolean– Uncontrolled default open stateonOpenChange?: (open: boolean) => void– Callback when state changesdisabled?: boolean– Disabled statecontainerStyle?: ViewStyle | ViewStyle[]– Container styletestID?: string
CollapsibleTrigger:
asChild?: boolean– Render as child component (for custom triggers)style?: ViewStyle | ViewStyle[]– Trigger styletestID?: string
CollapsibleContent:
forceMount?: boolean– Always render content (just hide visually)style?: ViewStyle | ViewStyle[]– Content styleanimationDuration?: number– Animation duration in ms (default: 300)testID?: string
Features
- ✅ Smooth height and opacity animations
- ✅ Controlled and uncontrolled modes
- ✅
asChildpattern for custom triggers - ✅
forceMountoption for always-mounted content - ✅ Customizable animation duration
- ✅ Accessibility support (expanded state, screen reader friendly)
- ✅ Disabled state support
- ✅ Theme-aware styling
- ✅ TypeScript-first with full type safety
Usage Examples
FAQ Section:
const faqs = [
{ question: "What is this?", answer: "This is..." },
{ question: "How does it work?", answer: "It works by..." },
];
{
faqs.map((faq, index) => (
<Collapsible
key={index}
defaultOpen={false}
>
<CollapsibleTrigger>
<View style={styles.faqHeader}>
<Text style={styles.question}>
{faq.question}
</Text>
<Text>▼</Text>
</View>
</CollapsibleTrigger>
<CollapsibleContent>
<Text style={styles.answer}>{faq.answer}</Text>
</CollapsibleContent>
</Collapsible>
));
}Accordion Menu:
const [openIndex, setOpenIndex] = useState<number | null>(null);
{
menuItems.map((item, index) => (
<Collapsible
key={index}
open={openIndex === index}
onOpenChange={(isOpen) =>
setOpenIndex(isOpen ? index : null)
}
>
<CollapsibleTrigger>
<Text>{item.title}</Text>
</CollapsibleTrigger>
<CollapsibleContent>
{item.children.map((child) => (
<Text key={child.id}>{child.name}</Text>
))}
</CollapsibleContent>
</Collapsible>
));
}Expandable Card:
<Card>
<Collapsible>
<CollapsibleTrigger>
<CardHeader>
<CardTitle>Product Details</CardTitle>
<Text>▼</Text>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent>
<Text>Detailed product information...</Text>
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>Best Practices
- Make triggers obvious with icons or "Show more" text
- Start collapsed for FAQ sections so users can scan questions
- Keep content focused—each collapsible should cover one topic
- Test with real content lengths to ensure smooth animations
- Limit nesting levels on mobile to avoid confusion
- Use
forceMountwhen you need to preserve form state or component lifecycle - Use controlled mode when you need to coordinate multiple collapsibles (accordion pattern)
Checkbox
Checkbox component for form data collection, multiple selections, and terms acceptance. Supports checked, unchecked, and indeterminate states.
import { Checkbox, Label } from "@tradly/react-native-ui";
// Basic
<Checkbox checked={isChecked} onCheckedChange={setIsChecked} />
// With label (pressable)
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<Label onPress={() => setIsChecked(!isChecked)}>
Accept terms and conditions
</Label>
<Checkbox
checked={isChecked}
onCheckedChange={setIsChecked}
/>
</View>
// Indeterminate state (for parent checkboxes)
<Checkbox
checked={someSelected ? "indeterminate" : allSelected}
onCheckedChange={handleChange}
/>
// Required field
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<Label required>I agree to the terms</Label>
<Checkbox required checked={agreed} onCheckedChange={setAgreed} />
</View>
// Form integration
<Checkbox
name="newsletter"
value="yes"
checked={subscribed}
onCheckedChange={setSubscribed}
/>Props
checked?: boolean | "indeterminate"– controlled checked statedefaultChecked?: boolean | "indeterminate"– uncontrolled initial stateonCheckedChange?: (checked: boolean | "indeterminate") => void– callback when toggleddisabled?: boolean– disabled staterequired?: boolean– required for form validationname?: string– form field namevalue?: string– form value (default:"on")style?: ViewStyle | ViewStyle[]– container styleboxStyle?: ViewStyle | ViewStyle[]– checkbox box styletestID?: string
Features
- ✅ Three states: checked, unchecked, and indeterminate
- ✅ Smooth spring animations for checkmark/indeterminate indicator
- ✅ Theme-aware colors
- ✅ Minimum 44px touch target for mobile
- ✅ Accessibility support (checkbox role, checked state)
- ✅ Controlled and uncontrolled modes
- ✅ Form integration (name, value, required props)
- ✅ Works seamlessly with
Labelcomponent
When to use Checkbox vs Switch
- Checkbox: Form data that needs submission, multiple selections from a list, terms acceptance, data collection fields
- Switch: Instant settings (dark mode, notifications), system preferences, feature flags, binary on/off states
Switch
Toggle switch component for binary on/off states. Perfect for settings, preferences, and instant state changes.
import { Switch, Label } from "@tradly/react-native-ui";
// Basic
<Switch checked={enabled} onCheckedChange={setEnabled} />
// With label
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<Label>Airplane mode</Label>
<Switch checked={airplaneMode} onCheckedChange={setAirplaneMode} />
</View>
// Settings panel example
<View>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 12 }}>
<View style={{ flex: 1 }}>
<Label>Marketing emails</Label>
<Text variant="muted">Receive emails about new products, features, and more.</Text>
</View>
<Switch
checked={marketingEmails}
onCheckedChange={setMarketingEmails}
/>
</View>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingVertical: 12 }}>
<View style={{ flex: 1 }}>
<Label>Security emails</Label>
<Text variant="muted">Receive emails about your account security.</Text>
</View>
<Switch
checked={securityEmails}
onCheckedChange={setSecurityEmails}
/>
</View>
</View>Props
checked?: boolean– controlled checked statedefaultChecked?: boolean– uncontrolled initial stateonCheckedChange?: (checked: boolean) => void– callback when toggleddisabled?: boolean– disabled statename?: string– form field namevalue?: string– form value (default:"on")required?: boolean– required for form validationstyle?: ViewStyle | ViewStyle[]– container styletrackStyle?: ViewStyle | ViewStyle[]– track (background) stylethumbStyle?: ViewStyle | ViewStyle[]– thumb (circle) styletestID?: string
Features
- ✅ Smooth spring animations for thumb movement
- ✅ Theme-aware colors (primary color when checked)
- ✅ Minimum 44px touch target for mobile
- ✅ Accessibility support (switch role, checked state)
- ✅ Controlled and uncontrolled modes
- ✅ Form integration (name, value, required props)
When to use Switch vs Checkbox
- Switch: Instant settings (dark mode, notifications), system preferences, feature flags, binary on/off states
- Checkbox: Form data that needs submission, multiple selections from a list, terms acceptance, data collection fields
Card
Composable card layout: header / content / footer and actions.
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardAction,
CardContent,
CardFooter,
Button,
} from "@tradly/react-native-ui";
<Card>
<CardHeader>
<View>
<CardTitle>Billing</CardTitle>
<CardDescription>
Manage your billing information
</CardDescription>
</View>
<CardAction>
<Button
size="icon"
variant="ghost"
>
{">"}
</Button>
</CardAction>
</CardHeader>
<CardContent>{/* form or content */}</CardContent>
<CardFooter>
<Button variant="secondary">Cancel</Button>
<Button>Save</Button>
</CardFooter>
</Card>;Card props
variant?: "default" | "outlined"style?: ViewStyle | ViewStyle[]- All
ViewProps
Other subcomponents are simple layout primitives (View + Text) and accept
style props.
Badge
Small label / status chip.
import { Badge } from "@tradly/react-native-ui";
<Badge>Badge</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
// Count with cap (e.g. notifications)
<Badge variant="destructive" max={99}>{120}</Badge> {/* 99+ */}
<Badge variant="secondary" max={20}>{8}</Badge> {/* 8 */}Props
variant?: "default" | "secondary" | "destructive" | "outline"max?: number– cap for numeric children (99+behavior)style?: ViewStyle | ViewStyle[]textStyle?: TextStyle | TextStyle[]
If children is a node (icon + text), it’s rendered as-is inside the pill.
Avatar
Avatar system inspired by shadcn’s Avatar (root + image + fallback).
import { Avatar, AvatarImage, AvatarFallback } from "@tradly/react-native-ui";
// Simple: auto image + initials fallback
<Avatar
size="lg"
source={{ uri: user.avatarUrl }}
fallback={user.name}
/>;
// Advanced composition
<Avatar
size="md"
onStatusChange={console.log}
>
<AvatarImage source={{ uri: user.avatarUrl }} />
<AvatarFallback delayMs={600}>{user.name}</AvatarFallback>
</Avatar>;Sizes: sm | md | lg | xl (mapped to pixel dimensions).
Status: idle | loading | loaded | error (available via callbacks).
AvatarFallback intelligently derives initials when you pass a name string, or
you can render custom content (icons, etc.).
Sheet
Edge-positioned sheet (bottom, top, left, right) with smooth slide animations.
import { Sheet, Button, Text } from "@tradly/react-native-ui";
const [open, setOpen] = useState(false);
<>
<Button onPress={() => setOpen(true)}>Open Sheet</Button>
<Sheet
open={open}
onOpenChange={setOpen}
side="bottom"
snapPoints={[0.7]}
>
<Text variant="h3">Edit profile</Text>
{/* content */}
</Sheet>
</>;Props
open?: boolean– controlled visibility (preferred)visible?: boolean– legacy aliasonOpenChange?(open: boolean)– called when sheet should closeonClose?()– optional close hookside?: "bottom" | "top" | "left" | "right"– default"bottom"snapPoints?: number[]– fraction(s) of screen dimension (0–1) for max sizecontainerStyle?: ViewStyle | ViewStyle[]contentStyle?: ViewStyle | ViewStyle[]
Spinner
Simple loading indicator wrapper.
import { Spinner } from "@tradly/react-native-ui";
<Spinner size="md" />
<Spinner size="lg" color="#6366f1" />Props
size?: "sm" | "md" | "lg"color?: string
Table
Responsive table component for displaying structured data. Supports headers, footers, alignment, width control, and performance optimizations for large datasets.
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
TableCaption,
Badge,
} from "@tradly/react-native-ui";
// Basic table
<Table>
<TableCaption position="top">
A list of your recent invoices.
</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead align="right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>
<Badge variant="default">Paid</Badge>
</TableCell>
<TableCell align="right">$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>INV002</TableCell>
<TableCell>
<Badge variant="secondary">Pending</Badge>
</TableCell>
<TableCell align="right">$150.00</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>
<Text variant="small" style={{ fontWeight: "600" }}>
Total
</Text>
</TableCell>
<TableCell align="right">
<Text variant="small" style={{ fontWeight: "600" }}>
$400.00
</Text>
</TableCell>
</TableRow>
</TableFooter>
</Table>
// Scrollable table for wide data
<Table scrollable>
<TableHeader>
<TableRow>
<TableHead width={100}>ID</TableHead>
<TableHead width={200}>Name</TableHead>
<TableHead width={150}>Email</TableHead>
<TableHead width={100} align="right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{/* rows */}
</TableBody>
</Table>
// With FlatList for large datasets (performance)
const invoices = [
{ id: "1", invoice: "INV001", status: "Paid", amount: 250 },
{ id: "2", invoice: "INV002", status: "Pending", amount: 150 },
// ... many more
];
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead align="right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody
useFlatList
data={invoices}
keyExtractor={(item) => item.id}
renderItem={(item) => (
<TableRow>
<TableCell>{item.invoice}</TableCell>
<TableCell>
<Badge>{item.status}</Badge>
</TableCell>
<TableCell align="right">${item.amount.toFixed(2)}</TableCell>
</TableRow>
)}
emptyComponent={
<View style={{ padding: 20 }}>
<Text>No invoices found</Text>
</View>
}
/>
</Table>
// Interactive rows
<TableBody>
<TableRow
onPress={() => navigateToDetail(invoice.id)}
pressedStyle={{ backgroundColor: colors.muted }}
>
<TableCell>{invoice.number}</TableCell>
<TableCell>{invoice.status}</TableCell>
</TableRow>
</TableBody>
// Custom alignment and width
<TableHeader>
<TableRow>
<TableHead width="30%" align="left">Name</TableHead>
<TableHead width="40%" align="center">Description</TableHead>
<TableHead width="30%" align="right">Price</TableHead>
</TableRow>
</TableHeader>Components
Table– Root container with optional horizontal scrollingTableCaption– Table description (position: "top" | "bottom")TableHeader– Header row container (styled background)TableBody– Body container (supports FlatList for performance)TableFooter– Footer row container (styled background)TableRow– Row container (supports onPress for interactivity)TableHead– Header cell (bold text, styled)TableCell– Data cell (supports text truncation)
Props
Table:
scrollable?: boolean– Enable horizontal scrollingstyle?: ViewStyle | ViewStyle[]
TableBody:
useFlatList?: boolean– Use FlatList for large datasetsdata?: any[]– Data array for FlatList moderenderItem?: (item, index) => ReactElement– Render functionkeyExtractor?: (item, index) => string– Key extractoremptyComponent?: ReactElement– Custom empty statestyle?: ViewStyle | ViewStyle[]
TableRow:
onPress?: () => void– Make row interactivepressedStyle?: ViewStyle | ViewStyle[]– Pressed state styledisabled?: boolean– Disable interactionstyle?: ViewStyle | ViewStyle[]
TableHead / TableCell:
align?: "left" | "center" | "right"– Text alignmentwidth?: number | string– Column width (number or percentage string like "30%")style?: ViewStyle | ViewStyle[]textStyle?: TextStyle | TextStyle[]numberOfLines?: number– Text truncation (TableCell only)
TableCaption:
position?: "top" | "bottom"– Caption positionstyle?: ViewStyle | ViewStyle[]textStyle?: TextStyle | TextStyle[]
Features
- ✅ Responsive design (horizontal scroll for wide tables)
- ✅ Flexible alignment (left, center, right per cell)
- ✅ Column width control (fixed or percentage)
- ✅ Performance optimized (FlatList support for large datasets)
- ✅ Interactive rows (onPress support)
- ✅ Empty states (custom empty component)
- ✅ Text truncation (numberOfLines prop)
- ✅ Theme-aware styling
- ✅ Accessible structure (semantic components)
- ✅ Footer support for totals/summaries
Best Practices
- Use
scrollableprop for tables with many columns on mobile - Use
useFlatListwithdataandrenderItemfor 100+ rows - Always include
TableCaptionfor accessibility (even if visually hidden) - Use consistent alignment: numbers right, text left
- Provide meaningful empty states instead of blank tables
- Test with extreme data (long text, missing values, many digits)
Pagination
Pagination component for navigating through pages of content. Optimized for React Native with onPress callbacks instead of URLs, and mobile-friendly modes. Supports multiple UI variants, sizes, and custom icons.
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationFirst,
PaginationPrevious,
PaginationNext,
PaginationLast,
PaginationEllipsis,
usePagination,
} from "@tradly/react-native-ui";
// Basic pagination with manual setup
const [currentPage, setCurrentPage] = useState(1);
const totalPages = 10;
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
disabled={currentPage === 1}
onPress={() =>
setCurrentPage((p) => Math.max(1, p - 1))
}
/>
</PaginationItem>
{/* Page numbers */}
<PaginationItem>
<PaginationLink
page={1}
isActive={currentPage === 1}
onPress={setCurrentPage}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink
page={2}
isActive={currentPage === 2}
onPress={setCurrentPage}
/>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationLink
page={10}
isActive={currentPage === 10}
onPress={setCurrentPage}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
disabled={currentPage === totalPages}
onPress={() =>
setCurrentPage((p) =>
Math.min(totalPages, p + 1)
)
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// Using usePagination hook (recommended)
const [currentPage, setCurrentPage] = useState(1);
const totalPages = 20;
const {
pageNumbers,
handlePageChange,
handlePrevious,
handleNext,
isFirstPage,
isLastPage,
} = usePagination({
currentPage,
totalPages,
onPageChange: setCurrentPage,
siblingCount: 1, // Pages to show on each side of current
showFirstLast: true, // Show first/last page buttons
mobileMode: false, // Show fewer pages on mobile
});
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
disabled={isFirstPage}
onPress={handlePrevious}
/>
</PaginationItem>
{pageNumbers.map((page, index) => {
if (page === "ellipsis-start" || page === "ellipsis-end") {
return (
<PaginationItem key={`ellipsis-${index}`}>
<PaginationEllipsis />
</PaginationItem>
);
}
return (
<PaginationItem key={page}>
<PaginationLink
page={page}
isActive={currentPage === page}
onPress={handlePageChange}
/>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
disabled={isLastPage}
onPress={handleNext}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// UI Variants
<Pagination variant="outlined" size="md">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
variant="outlined"
size="md"
disabled={isFirstPage}
onPress={handlePrevious}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink
variant="outlined"
size="md"
page={1}
isActive={currentPage === 1}
onPress={handlePageChange}
/>
</PaginationItem>
{/* ... more pages ... */}
<PaginationItem>
<PaginationNext
variant="outlined"
size="md"
disabled={isLastPage}
onPress={handleNext}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// Different sizes
<Pagination size="sm">
{/* Small pagination */}
</Pagination>
<Pagination size="lg">
{/* Large pagination */}
</Pagination>
// With First/Last buttons (variant 1 from design guide)
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationFirst
disabled={isFirstPage}
onPress={handleFirst}
/>
</PaginationItem>
<PaginationItem>
<PaginationPrevious
disabled={isFirstPage}
onPress={handlePrevious}
/>
</PaginationItem>
{/* ... pages ... */}
<PaginationItem>
<PaginationNext
disabled={isLastPage}
onPress={handleNext}
/>
</PaginationItem>
<PaginationItem>
<PaginationLast
disabled={isLastPage}
onPress={handleLast}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// Circular variant (variant 2 from design guide)
<Pagination variant="circular">
<PaginationContent>
<PaginationItem>
<PaginationFirst
variant="circular"
disabled={isFirstPage}
onPress={handleFirst}
/>
</PaginationItem>
<PaginationItem>
<PaginationPrevious
variant="circular"
disabled={isFirstPage}
onPress={handlePrevious}
/>
</PaginationItem>
{pageNumbers.map((page, index) => {
if (page === "ellipsis-start" || page === "ellipsis-end") {
return (
<PaginationItem key={`ellipsis-${index}`}>
<PaginationEllipsis />
</PaginationItem>
);
}
return (
<PaginationItem key={page}>
<PaginationLink
variant="circular"
shape="circle"
page={page}
isActive={currentPage === page}
onPress={handlePageChange}
/>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
variant="circular"
disabled={isLastPage}
onPress={handleNext}
/>
</PaginationItem>
<PaginationItem>
<PaginationLast
variant="circular"
disabled={isLastPage}
onPress={handleLast}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// With custom icons
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
icon="←" // String emoji/icon
label="Prev"
onPress={handlePrevious}
/>
</PaginationItem>
{/* ... pages ... */}
<PaginationItem>
<PaginationNext
icon={require('./assets/next-icon.png')} // Image source
label="Next"
onPress={handleNext}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// With React element icons
import { ChevronLeft, ChevronRight } from 'lucide-react-native';
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
icon={<ChevronLeft size={16} />}
onPress={handlePrevious}
/>
</PaginationItem>
{/* ... pages ... */}
<PaginationItem>
<PaginationNext
icon={<ChevronRight size={16} />}
onPress={handleNext}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// Using hook with variants and icons
const {
pageNumbers,
handlePageChange,
handlePrevious,
handleNext,
isFirstPage,
isLastPage,
variant,
size,
previousIcon,
nextIcon,
showIcons,
} = usePagination({
currentPage,
totalPages,
onPageChange: setCurrentPage,
variant: "outlined",
size: "lg",
icons: {
previous: "←",
next: "→",
},
showIcons: true,
});
<Pagination variant={variant} size={size}>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
variant={variant}
size={size}
icon={previousIcon}
showIcon={showIcons}
disabled={isFirstPage}
onPress={handlePrevious}
/>
</PaginationItem>
{pageNumbers.map((page, index) => {
if (page === "ellipsis-start" || page === "ellipsis-end") {
return (
<PaginationItem key={`ellipsis-${index}`}>
<PaginationEllipsis />
</PaginationItem>
);
}
return (
<PaginationItem key={page}>
<PaginationLink
variant={variant}
size={size}
page={page}
isActive={currentPage === page}
onPress={handlePageChange}
/>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
variant={variant}
size={size}
icon={nextIcon}
showIcon={showIcons}
disabled={isLastPage}
onPress={handleNext}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;
// Mobile-friendly mode (shows fewer pages)
const { pageNumbers, handlePageChange, handlePrevious, handleNext } =
usePagination({
currentPage,
totalPages: 50,
onPageChange: setCurrentPage,
mobileMode: true, // Shows only 3-5 pages
});
// With loading state
const [loading, setLoading] = useState(false);
const handlePageChange = async (page: number) => {
setLoading(true);
await fetchData(page);
setCurrentPage(page);
setLoading(false);
};
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
disabled={isFirstPage || loading}
onPress={handlePrevious}
/>
</PaginationItem>
{/* ... pages ... */}
<PaginationItem>
<PaginationNext
disabled={isLastPage || loading}
onPress={handleNext}
/>
</PaginationItem>
</PaginationContent>
</Pagination>;Components
Pagination– Root containerPaginationContent– Items container (groups all elements)PaginationItem– Individual item wrapperPaginationLink– Page number buttonPaginationFirst– First page button (jump to page 1)PaginationPrevious– Previous page buttonPaginationNext– Next page buttonPaginationLast– Last page button (jump to last page)PaginationEllipsis– Omitted pages indicator (…)
Props
Pagination:
variant?: "default" | "outlined" | "minimal" | "filled"– Visual style variantsize?: "sm" | "md" | "lg"– Size of pagination buttonsstyle?: ViewStyle | ViewStyle[]testID?: string
PaginationLink:
page?: number– Page number (1-indexed)isActive?: boolean– Highlight current pagedisabled?: boolean– Disabled statevariant?: "default" | "outlined" | "minimal" | "filled" | "circular"– Visual style variantsize?: "sm" | "md" | "lg"– Size of the buttonshape?: "square" | "circle"– Button shape (square or circle)onPress?: (page: number) => void– Callback when pressedstyle?: ViewStyle | ViewStyle[]textStyle?: TextStyle | TextStyle[]
PaginationFirst / PaginationLast:
disabled?: boolean– Disabled state (typically true on first/last page)variant?: "default" | "outlined" | "minimal" | "filled" | "circular"– Visual style variantsize?: "sm" | "md" | "lg"– Size of the buttononPress?: () => void– Callback when pressedlabel?: string– Custom label (optional, icon shown by default)showIcon?: boolean– Show icon (default: true)icon?: ReactElement | ImageSourcePropType | string– Custom icon (default: "«" for First, "»" for Last)style?: ViewStyle | ViewStyle[]textStyle?: TextStyle | TextStyle[]
PaginationPrevious / PaginationNext:
disabled?: boolean– Disabled state (typically true on first/last page)- `variant?: "default" | "outlined"
