npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-ui

Peer deps (usually already installed in Expo apps):

npm install react react-native react-native-gesture-handler react-native-reanimated

Quick 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, cardForeground
  • primary, primaryForeground (buttons/links/toggles)
  • secondary, secondaryForeground
  • muted, mutedForeground (disabled/subtle)
  • accent, accentForeground (highlights)
  • destructive, destructiveForeground
  • border, 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.fontSizesxs5xl
  • typography.fontWeightsnormal | medium | semibold | bold
  • typography.lineHeightstight | normal | relaxed
  • typography.letterSpacingstight | normal | wide
  • typography.fontssans | 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?: boolean
  • loading?: boolean
  • asChild?: boolean
  • onPress?: () => void
  • style?: 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 (except onChange)
  • label?: string
  • error?: string
  • containerStyle?: ViewStyle | ViewStyle[]
  • inputStyle?: TextStyle | TextStyle[]
  • onChange?(value: string): void – convenience wrapper around onChangeText
  • variant?: "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 (except onChange, multiline is always true)
  • label?: string – Label text
  • description?: string – Helper/description text below textarea
  • error?: string – Error message
  • required?: boolean – Mark field as required (adds asterisk to label)
  • rows?: number – Number of rows (affects initial height, default: 4)
  • maxLength?: number – Maximum character length
  • showCharCount?: boolean – Show character counter
  • containerStyle?: ViewStyle | ViewStyle[]
  • textareaStyle?: TextStyle | TextStyle[]
  • onChange?(value: string): void – Convenience wrapper around onChangeText

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 rows prop
  • ✅ Theme-aware styling
  • ✅ Text alignment at top (Android)

Best Practices

  • Set appropriate rows based on expected content (3 for comments, 6-8 for feedback, 10+ for content creation)
  • Show character limits before users hit them with showCharCount
  • Use description for 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 changes
  • label?: string – Label text
  • disabled?: boolean – Disabled state
  • showPresets?: 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 RGB
  • rgbToHex(r: number, g: number, b: number): string – Convert RGB to hex
  • rgbToHsl(r: number, g: number, b: number): HSL – Convert RGB to HSL
  • hslToRgb(h: number, s: number, l: number): RGB – Convert HSL to RGB
  • isValidHex(hex: string): boolean – Validate hex color
  • normalizeHex(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 array
  • defaultValue?: string[] – Uncontrolled default values (default: [])
  • onChange?: (values: string[]) => void – Callback when values change
  • label?: string – Label text
  • placeholder?: string – Placeholder text (default: "Type and press enter or comma")
  • error?: string – Error message
  • required?: boolean – Mark field as required
  • disabled?: boolean – Disabled state
  • maxValues?: number – Maximum number of values allowed
  • separators?: string[] – Separator characters that trigger tag creation (default: [",", "\n"])
  • validateValue?: (value: string) => boolean | string – Validate each value before adding (return false or 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/chips
  • tagTextStyle?: TextStyle | TextStyle[] – Style for tag text
  • testID?: 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 validateValue to ensure data quality (email format, URL format, etc.)
  • Use transformValue to normalize values (trim whitespace, lowercase, etc.)
  • Set maxValues to 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 list
  • onFilesChange?: (files: FileItem[]) => void – Callback when files change
  • picker: FilePickerFnRequired 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 server
  • label?: string – Label text
  • accept?: "image" | "video" | "file" | "all" – Accept file types (default: "all")
  • multiple?: boolean – Allow multiple files (default: false)
  • maxFiles?: number – Maximum number of files
  • maxSize?: number – Maximum file size in bytes
  • showProgress?: boolean – Show upload progress (default: true)
  • autoUpload?: boolean – Auto upload after selection (default: false)
  • disabled?: boolean – Disabled state
  • error?: string – Error message
  • required?: boolean – Required field
  • transformPickerResult?: (result: PickerResult) => FileItem – Transform picker result to FileItem
  • layout?: "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 button
  • containerStyle, 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 transformPickerResult function if your picker returns a different format
  • Use validateValue in the transform function for additional validation
  • Implement proper error handling in your uploader function
  • Call onProgress callback during upload for real-time progress updates
  • Use remover function 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 value
  • onValueChange?: (value: string | string[]) => void
  • placeholder?: string – default "Select an option"
  • options?: SelectOption[] – flat list of options
  • groups?: SelectGroup[] – grouped options with labels
  • multiple?: boolean – enable multi-select mode
  • searchable?: boolean – enable search/filter
  • searchPlaceholder?: string – default "Search..."
  • disabled?: boolean
  • position?: "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 container
  • testID?: 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 value
  • onSelect?: (date: Date | Date[] | DateRange | null) => void – Selection handler
  • disabledDates?: DateMatcher | DateMatcher[] – Dates to disable
  • minDate?: Date – Minimum selectable date
  • maxDate?: Date – Maximum selectable date
  • displayMode?: "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 required
  • disabled?: boolean – Disabled state
  • error?: string – Error message
  • captionLayout?: "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 icons
  • containerStyle, calendarStyle – Custom styles
  • testID?: 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 dates

DateRange 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 minDate to disable past dates for booking systems
  • Use disabledDates with { 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 time
  • defaultValue?: Time | null – Uncontrolled default time
  • onChange?: (time: Time | null) => void – Time change handler
  • format?: "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 required
  • disabled?: boolean – Disabled state
  • error?: string – Error message
  • minTime?: Time – Minimum selectable time
  • maxTime?: Time – Maximum selectable time
  • minuteStep?: number – Step for minutes (default: 1, e.g., 5 for 5-minute intervals)
  • showSeconds?: boolean – Show seconds picker (default: false)
  • icons?: { clock? } – Custom clock icon
  • containerStyle, timePickerStyle – Custom styles
  • testID?: 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 minTime and maxTime for business hours or appointment scheduling
  • Use minuteStep={5} or minuteStep={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 state
  • CollapsibleTrigger – Clickable element to toggle open/closed
  • CollapsibleContent – Content that expands/collapses

Props

Collapsible:

  • open?: boolean – Controlled open state
  • defaultOpen?: boolean – Uncontrolled default open state
  • onOpenChange?: (open: boolean) => void – Callback when state changes
  • disabled?: boolean – Disabled state
  • containerStyle?: ViewStyle | ViewStyle[] – Container style
  • testID?: string

CollapsibleTrigger:

  • asChild?: boolean – Render as child component (for custom triggers)
  • style?: ViewStyle | ViewStyle[] – Trigger style
  • testID?: string

CollapsibleContent:

  • forceMount?: boolean – Always render content (just hide visually)
  • style?: ViewStyle | ViewStyle[] – Content style
  • animationDuration?: number – Animation duration in ms (default: 300)
  • testID?: string

Features

  • ✅ Smooth height and opacity animations
  • ✅ Controlled and uncontrolled modes
  • asChild pattern for custom triggers
  • forceMount option 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 forceMount when 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 state
  • defaultChecked?: boolean | "indeterminate" – uncontrolled initial state
  • onCheckedChange?: (checked: boolean | "indeterminate") => void – callback when toggled
  • disabled?: boolean – disabled state
  • required?: boolean – required for form validation
  • name?: string – form field name
  • value?: string – form value (default: "on")
  • style?: ViewStyle | ViewStyle[] – container style
  • boxStyle?: ViewStyle | ViewStyle[] – checkbox box style
  • testID?: 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 Label component

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 state
  • defaultChecked?: boolean – uncontrolled initial state
  • onCheckedChange?: (checked: boolean) => void – callback when toggled
  • disabled?: boolean – disabled state
  • name?: string – form field name
  • value?: string – form value (default: "on")
  • required?: boolean – required for form validation
  • style?: ViewStyle | ViewStyle[] – container style
  • trackStyle?: ViewStyle | ViewStyle[] – track (background) style
  • thumbStyle?: ViewStyle | ViewStyle[] – thumb (circle) style
  • testID?: 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 alias
  • onOpenChange?(open: boolean) – called when sheet should close
  • onClose?() – optional close hook
  • side?: "bottom" | "top" | "left" | "right" – default "bottom"
  • snapPoints?: number[] – fraction(s) of screen dimension (0–1) for max size
  • containerStyle?: 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 scrolling
  • TableCaption – 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 scrolling
  • style?: ViewStyle | ViewStyle[]

TableBody:

  • useFlatList?: boolean – Use FlatList for large datasets
  • data?: any[] – Data array for FlatList mode
  • renderItem?: (item, index) => ReactElement – Render function
  • keyExtractor?: (item, index) => string – Key extractor
  • emptyComponent?: ReactElement – Custom empty state
  • style?: ViewStyle | ViewStyle[]

TableRow:

  • onPress?: () => void – Make row interactive
  • pressedStyle?: ViewStyle | ViewStyle[] – Pressed state style
  • disabled?: boolean – Disable interaction
  • style?: ViewStyle | ViewStyle[]

TableHead / TableCell:

  • align?: "left" | "center" | "right" – Text alignment
  • width?: 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 position
  • style?: 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 scrollable prop for tables with many columns on mobile
  • Use useFlatList with data and renderItem for 100+ rows
  • Always include TableCaption for 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 container
  • PaginationContent – Items container (groups all elements)
  • PaginationItem – Individual item wrapper
  • PaginationLink – Page number button
  • PaginationFirst – First page button (jump to page 1)
  • PaginationPrevious – Previous page button
  • PaginationNext – Next page button
  • PaginationLast – Last page button (jump to last page)
  • PaginationEllipsis – Omitted pages indicator (…)

Props

Pagination:

  • variant?: "default" | "outlined" | "minimal" | "filled" – Visual style variant
  • size?: "sm" | "md" | "lg" – Size of pagination buttons
  • style?: ViewStyle | ViewStyle[]
  • testID?: string

PaginationLink:

  • page?: number – Page number (1-indexed)
  • isActive?: boolean – Highlight current page
  • disabled?: boolean – Disabled state
  • variant?: "default" | "outlined" | "minimal" | "filled" | "circular" – Visual style variant
  • size?: "sm" | "md" | "lg" – Size of the button
  • shape?: "square" | "circle" – Button shape (square or circle)
  • onPress?: (page: number) => void – Callback when pressed
  • style?: 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 variant
  • size?: "sm" | "md" | "lg" – Size of the button
  • onPress?: () => void – Callback when pressed
  • label?: 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"