reusable-ui-components — community reusable-ui-components, crispy, community, ide skills, Claude Code, Cursor, Windsurf

v1.0.0
GitHub

About this Skill

Perfect for Frontend Agents needing portable and native-first UI components for Expo Router apps Free UI components I use for building Expo Router apps

EvanBacon EvanBacon
[0]
[0]
Updated: 3/5/2026

Agent Capability Analysis

The reusable-ui-components skill by EvanBacon is an open-source community AI agent skill for Claude Code and other IDE workflows, helping agents execute tasks with better context, repeatability, and domain-specific guidance.

Ideal Agent Persona

Perfect for Frontend Agents needing portable and native-first UI components for Expo Router apps

Core Value

Empowers agents to build production-quality UI components inspired by shadcn/ui, Base UI, Radix, and Konsta UI, following iOS San Francisco design guidelines with liquid glass aesthetics and utilizing native primitives with graceful fallbacks, all while prioritizing portability and copy-paste readiness

Capabilities Granted for reusable-ui-components

Building reusable UI components for Expo Router apps
Creating native-first and portable UI elements with liquid glass aesthetics
Designing UI components that follow iOS San Francisco design guidelines

! Prerequisites & Limits

  • Requires Expo Router for app development
  • Limited to iOS San Francisco design guidelines and liquid glass aesthetics
Labs Demo

Browser Sandbox Environment

⚡️ Ready to unleash?

Experience this Agent in a zero-setup browser environment powered by WebContainers. No installation required.

Boot Container Sandbox

reusable-ui-components

Install reusable-ui-components, an AI agent skill for AI agent workflows and automation. Works with Claude Code, Cursor, and Windsurf with one-command setup.

SKILL.md
Readonly

Creating Reusable UI Components for Expo Router

This guide covers building production-quality, portable UI components inspired by shadcn/ui, Base UI, Radix, and Konsta UI. Components follow iOS San Francisco design guidelines with liquid glass aesthetics and prioritize native primitives with graceful fallbacks.

Philosophy

Core Principles

  1. Portable & Copy-Paste Ready - Components should be self-contained and easy to copy between projects
  2. Native-First - Always check for Expo Router primitives before building custom solutions
  3. iOS Design Language - Use San Francisco style guide as the baseline for all platforms
  4. Compound Components - Break complex components into composable sub-components
  5. CSS Variables for Customization - Use design tokens for theming, not hardcoded values
  6. Accessibility Built-In - Keyboard handling, safe areas, and screen reader support by default

Inspiration Sources

LibraryLearn From
shadcn/uiComponent structure, copy-paste architecture
Radix UICompound component patterns, accessibility primitives
Base UIHeadless component APIs, composition patterns
Konsta UIiOS liquid glass aesthetics, platform-adaptive styling

Component File Structure

src/components/ui/
├── button.tsx          # Default (shared) implementation
├── button.ios.tsx      # iOS-specific overrides (optional)
├── button.web.tsx      # Web-specific overrides (optional)
└── button.android.tsx  # Android-specific overrides (optional)

Metro Resolution Priority:

  1. .ios.tsx / .android.tsx / .web.tsx (platform-specific)
  2. .native.tsx (iOS + Android)
  3. .tsx (fallback for all platforms)

Design Tokens & CSS Variables

Global Theme Variables

Define customizable design tokens in src/global.css:

css
1@import "tailwindcss/theme.css" layer(theme); 2@import "tailwindcss/preflight.css" layer(base); 3@import "tailwindcss/utilities.css"; 4 5/* Import Apple system colors */ 6@import "./css/sf.css"; 7 8@layer theme { 9 @theme { 10 /* Typography Scale */ 11 --font-sans: system-ui; 12 --font-mono: ui-monospace; 13 --font-rounded: ui-rounded; 14 15 /* Component Tokens */ 16 --component-radius: 12px; 17 --component-radius-lg: 16px; 18 --component-radius-full: 9999px; 19 20 /* Spacing Scale */ 21 --spacing-xs: 4px; 22 --spacing-sm: 8px; 23 --spacing-md: 12px; 24 --spacing-lg: 16px; 25 --spacing-xl: 24px; 26 27 /* Animation */ 28 --transition-fast: 150ms; 29 --transition-normal: 200ms; 30 --transition-slow: 300ms; 31 } 32} 33 34/* Platform-specific overrides */ 35@media ios { 36 :root { 37 --font-sans: system-ui; 38 --font-rounded: ui-rounded; 39 --component-radius: 10px; 40 } 41} 42 43@media android { 44 :root { 45 --font-sans: normal; 46 --font-rounded: normal; 47 --component-radius: 8px; 48 } 49}

Apple System Colors

Create platform-adaptive colors in src/css/sf.css:

css
1@layer base { 2 html { 3 color-scheme: light dark; 4 } 5} 6 7:root { 8 /* Primary Colors */ 9 --sf-blue: light-dark(rgb(0 122 255), rgb(10 132 255)); 10 --sf-green: light-dark(rgb(52 199 89), rgb(48 209 89)); 11 --sf-red: light-dark(rgb(255 59 48), rgb(255 69 58)); 12 --sf-orange: light-dark(rgb(255 149 0), rgb(255 159 10)); 13 --sf-yellow: light-dark(rgb(255 204 0), rgb(255 214 10)); 14 --sf-purple: light-dark(rgb(175 82 222), rgb(191 90 242)); 15 --sf-pink: light-dark(rgb(255 45 85), rgb(255 55 95)); 16 17 /* Gray Scale */ 18 --sf-gray: light-dark(rgb(142 142 147), rgb(142 142 147)); 19 --sf-gray-2: light-dark(rgb(174 174 178), rgb(99 99 102)); 20 --sf-gray-3: light-dark(rgb(199 199 204), rgb(72 72 74)); 21 --sf-gray-4: light-dark(rgb(209 209 214), rgb(58 58 60)); 22 --sf-gray-5: light-dark(rgb(229 229 234), rgb(44 44 46)); 23 --sf-gray-6: light-dark(rgb(242 242 247), rgb(28 28 30)); 24 25 /* Text Colors */ 26 --sf-text: light-dark(rgb(0 0 0), rgb(255 255 255)); 27 --sf-text-2: light-dark(rgb(60 60 67 / 0.6), rgb(235 235 245 / 0.6)); 28 --sf-text-3: light-dark(rgb(60 60 67 / 0.3), rgb(235 235 245 / 0.3)); 29 --sf-text-placeholder: light-dark(rgb(60 60 67 / 0.3), rgb(235 235 245 / 0.3)); 30 31 /* Background Colors */ 32 --sf-bg: light-dark(rgb(255 255 255), rgb(0 0 0)); 33 --sf-bg-2: light-dark(rgb(242 242 247), rgb(28 28 30)); 34 --sf-grouped-bg: light-dark(rgb(242 242 247), rgb(0 0 0)); 35 --sf-grouped-bg-2: light-dark(rgb(255 255 255), rgb(28 28 30)); 36 37 /* Border & Fill */ 38 --sf-border: light-dark(rgb(60 60 67 / 0.12), rgb(84 84 88 / 0.65)); 39 --sf-fill: light-dark(rgb(120 120 128 / 0.2), rgb(120 120 128 / 0.32)); 40 41 /* Link Color */ 42 --sf-link: var(--sf-blue); 43} 44 45/* iOS: Use native platform colors */ 46@media ios { 47 :root { 48 --sf-blue: platformColor(systemBlue); 49 --sf-green: platformColor(systemGreen); 50 --sf-red: platformColor(systemRed); 51 --sf-orange: platformColor(systemOrange); 52 --sf-yellow: platformColor(systemYellow); 53 --sf-purple: platformColor(systemPurple); 54 --sf-pink: platformColor(systemPink); 55 --sf-gray: platformColor(systemGray); 56 --sf-gray-2: platformColor(systemGray2); 57 --sf-gray-3: platformColor(systemGray3); 58 --sf-gray-4: platformColor(systemGray4); 59 --sf-gray-5: platformColor(systemGray5); 60 --sf-gray-6: platformColor(systemGray6); 61 --sf-text: platformColor(label); 62 --sf-text-2: platformColor(secondaryLabel); 63 --sf-text-3: platformColor(tertiaryLabel); 64 --sf-text-placeholder: platformColor(placeholderText); 65 --sf-bg: platformColor(systemBackground); 66 --sf-bg-2: platformColor(secondarySystemBackground); 67 --sf-grouped-bg: platformColor(systemGroupedBackground); 68 --sf-grouped-bg-2: platformColor(secondarySystemGroupedBackground); 69 --sf-border: platformColor(separator); 70 --sf-fill: platformColor(tertiarySystemFill); 71 --sf-link: platformColor(link); 72 } 73} 74 75/* Register as Tailwind theme colors */ 76@layer theme { 77 @theme { 78 --color-sf-blue: var(--sf-blue); 79 --color-sf-green: var(--sf-green); 80 --color-sf-red: var(--sf-red); 81 --color-sf-orange: var(--sf-orange); 82 --color-sf-yellow: var(--sf-yellow); 83 --color-sf-purple: var(--sf-purple); 84 --color-sf-pink: var(--sf-pink); 85 --color-sf-gray: var(--sf-gray); 86 --color-sf-gray-2: var(--sf-gray-2); 87 --color-sf-gray-3: var(--sf-gray-3); 88 --color-sf-gray-4: var(--sf-gray-4); 89 --color-sf-gray-5: var(--sf-gray-5); 90 --color-sf-gray-6: var(--sf-gray-6); 91 --color-sf-text: var(--sf-text); 92 --color-sf-text-2: var(--sf-text-2); 93 --color-sf-text-3: var(--sf-text-3); 94 --color-sf-text-placeholder: var(--sf-text-placeholder); 95 --color-sf-bg: var(--sf-bg); 96 --color-sf-bg-2: var(--sf-bg-2); 97 --color-sf-grouped-bg: var(--sf-grouped-bg); 98 --color-sf-grouped-bg-2: var(--sf-grouped-bg-2); 99 --color-sf-border: var(--sf-border); 100 --color-sf-fill: var(--sf-fill); 101 --color-sf-link: var(--sf-link); 102 } 103}

Accessing CSS Variables in JavaScript

tsx
1import { useCSSVariable } from "@/tw"; 2 3function MyComponent() { 4 const primaryColor = useCSSVariable("--sf-blue"); 5 const borderColor = useCSSVariable("--sf-border"); 6 7 return ( 8 <View style={{ borderColor }}> 9 <Text style={{ color: primaryColor }}>Hello</Text> 10 </View> 11 ); 12}

Compound Component Pattern

Use compound components for complex, multi-element UI. This provides flexibility while maintaining cohesive behavior.

Template Structure

tsx
1"use client"; 2 3import React, { createContext, use } from "react"; 4import { View, Text, Pressable } from "@/tw"; 5import { cn } from "@/lib/utils"; 6import type { ViewProps, TextProps } from "react-native"; 7 8// 1. Define Context for shared state 9interface ComponentContextValue { 10 variant: "default" | "outline" | "ghost"; 11 size: "sm" | "md" | "lg"; 12 disabled?: boolean; 13} 14 15const ComponentContext = createContext<ComponentContextValue | null>(null); 16 17function useComponentContext() { 18 const context = use(ComponentContext); 19 if (!context) { 20 throw new Error("Component parts must be used within Component.Root"); 21 } 22 return context; 23} 24 25// 2. Root component provides context 26interface RootProps extends ViewProps { 27 variant?: ComponentContextValue["variant"]; 28 size?: ComponentContextValue["size"]; 29 disabled?: boolean; 30} 31 32function Root({ 33 variant = "default", 34 size = "md", 35 disabled, 36 children, 37 className, 38 ...props 39}: RootProps) { 40 return ( 41 <ComponentContext value={{ variant, size, disabled }}> 42 <View 43 {...props} 44 className={cn( 45 "flex-row items-center", 46 disabled && "opacity-50", 47 className 48 )} 49 > 50 {children} 51 </View> 52 </ComponentContext> 53 ); 54} 55 56// 3. Sub-components consume context 57function Label({ className, ...props }: TextProps) { 58 const { size } = useComponentContext(); 59 60 return ( 61 <Text 62 {...props} 63 className={cn( 64 "text-sf-text", 65 size === "sm" && "text-sm", 66 size === "md" && "text-base", 67 size === "lg" && "text-lg", 68 className 69 )} 70 /> 71 ); 72} 73 74function Icon({ className, ...props }: ViewProps) { 75 const { size } = useComponentContext(); 76 77 const sizeClass = { 78 sm: "w-4 h-4", 79 md: "w-5 h-5", 80 lg: "w-6 h-6", 81 }[size]; 82 83 return ( 84 <View {...props} className={cn(sizeClass, className)} /> 85 ); 86} 87 88// 4. Export as compound component 89export const Component = { 90 Root, 91 Label, 92 Icon, 93}; 94 95// 5. Convenience export for simple usage 96export function SimpleComponent(props: RootProps & { label: string }) { 97 const { label, ...rootProps } = props; 98 return ( 99 <Component.Root {...rootProps}> 100 <Component.Label>{label}</Component.Label> 101 </Component.Root> 102 ); 103}

Native-First Component Development

Check for Expo Router Primitives First

Before building custom components, check if Expo Router or Expo provides a native primitive:

Component NeedCheck First
Navigation Stackexpo-router Stack
Tab Navigationexpo-router Tabs
Modals/Sheetspresentation: "modal" or presentation: "formSheet"
Linksexpo-router Link
Iconsexpo-symbols (SF Symbols)
Date Picker@react-native-community/datetimepicker
Segmented Control@react-native-segmented-control/segmented-control
Blur Effectsexpo-blur or expo-glass-effect
Hapticsexpo-haptics
Safe Areasreact-native-safe-area-context

Platform Detection

tsx
1// Check current platform 2if (process.env.EXPO_OS === "ios") { 3 // iOS-specific behavior 4} else if (process.env.EXPO_OS === "android") { 5 // Android-specific behavior 6} else if (process.env.EXPO_OS === "web") { 7 // Web-specific behavior 8} 9 10// Check for specific features 11import { isLiquidGlassAvailable } from "expo-glass-effect"; 12const GLASS = isLiquidGlassAvailable(); // iOS 26+ liquid glass

Platform-Specific File Example: Switch

switch.tsx (default - re-exports native):

tsx
1export { Switch, type SwitchProps } from "react-native";

switch.web.tsx (web - iOS-styled custom):

tsx
1"use client"; 2 3import { useState, useRef, useEffect } from "react"; 4import { 5 View, 6 Animated, 7 PanResponder, 8 StyleSheet, 9 Pressable, 10} from "react-native"; 11 12export type SwitchProps = { 13 value?: boolean; 14 onValueChange?: (value: boolean) => void; 15 disabled?: boolean; 16 thumbColor?: string; 17 trackColor?: { true: string; false: string }; 18 ios_backgroundColor?: string; 19}; 20 21export function Switch({ 22 value = false, 23 onValueChange, 24 disabled = false, 25 thumbColor = "#fff", 26 trackColor = { true: "#34C759", false: "#E9E9EA" }, 27 ios_backgroundColor, 28}: SwitchProps) { 29 const [isOn, setIsOn] = useState(value); 30 const animatedValue = useRef(new Animated.Value(value ? 1 : 0)).current; 31 32 useEffect(() => { 33 setIsOn(value); 34 Animated.spring(animatedValue, { 35 toValue: value ? 1 : 0, 36 useNativeDriver: false, 37 friction: 8, 38 tension: 40, 39 }).start(); 40 }, [value, animatedValue]); 41 42 const toggle = () => { 43 if (disabled) return; 44 const newValue = !isOn; 45 setIsOn(newValue); 46 onValueChange?.(newValue); 47 }; 48 49 const translateX = animatedValue.interpolate({ 50 inputRange: [0, 1], 51 outputRange: [2, 22], 52 }); 53 54 const bgColor = animatedValue.interpolate({ 55 inputRange: [0, 1], 56 outputRange: [ 57 ios_backgroundColor || trackColor.false, 58 trackColor.true, 59 ], 60 }); 61 62 return ( 63 <Pressable onPress={toggle} disabled={disabled}> 64 <Animated.View 65 style={[ 66 styles.track, 67 { backgroundColor: bgColor }, 68 disabled && styles.disabled, 69 ]} 70 > 71 <Animated.View 72 style={[ 73 styles.thumb, 74 { backgroundColor: thumbColor, transform: [{ translateX }] }, 75 ]} 76 /> 77 </Animated.View> 78 </Pressable> 79 ); 80} 81 82const styles = StyleSheet.create({ 83 track: { 84 width: 51, 85 height: 31, 86 borderRadius: 15.5, 87 justifyContent: "center", 88 }, 89 thumb: { 90 width: 27, 91 height: 27, 92 borderRadius: 13.5, 93 shadowColor: "#000", 94 shadowOffset: { width: 0, height: 2 }, 95 shadowOpacity: 0.2, 96 shadowRadius: 2, 97 elevation: 2, 98 }, 99 disabled: { 100 opacity: 0.5, 101 }, 102});

Accessibility Patterns

Keyboard Avoidance

For forms with text input, proper keyboard handling is critical:

tsx
1import { 2 useReanimatedKeyboardAnimation, 3 useKeyboardHandler, 4} from "react-native-keyboard-controller"; 5import { useAnimatedStyle } from "react-native-reanimated"; 6import { useSafeAreaInsets } from "react-native-safe-area-context"; 7 8function KeyboardAwareForm({ children }: { children: React.ReactNode }) { 9 const { bottom } = useSafeAreaInsets(); 10 const { height, progress } = useReanimatedKeyboardAnimation(); 11 12 const animatedStyle = useAnimatedStyle(() => ({ 13 paddingBottom: Math.max(bottom, Math.abs(height.value)), 14 })); 15 16 return ( 17 <Animated.View style={[{ flex: 1 }, animatedStyle]}> 18 {children} 19 </Animated.View> 20 ); 21}

Safe Area Handling

Always account for safe areas on notched devices:

tsx
1import { useSafeAreaInsets } from "react-native-safe-area-context"; 2 3function SafeContainer({ children }: { children: React.ReactNode }) { 4 const { top, bottom, left, right } = useSafeAreaInsets(); 5 6 return ( 7 <View 8 style={{ 9 flex: 1, 10 paddingTop: top, 11 paddingBottom: bottom, 12 paddingLeft: left, 13 paddingRight: right, 14 }} 15 > 16 {children} 17 </View> 18 ); 19}

Form Accessibility Pattern

tsx
1import { View, Text, TextInput } from "@/tw"; 2import { useSafeAreaInsets } from "react-native-safe-area-context"; 3import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; 4 5interface FormFieldProps { 6 label: string; 7 hint?: string; 8 error?: string; 9 children: React.ReactNode; 10} 11 12function FormField({ label, hint, error, children }: FormFieldProps) { 13 return ( 14 <View className="gap-1"> 15 <Text 16 className="text-sf-text-2 text-sm font-medium" 17 accessibilityRole="text" 18 > 19 {label} 20 </Text> 21 {children} 22 {hint && !error && ( 23 <Text className="text-sf-text-3 text-xs">{hint}</Text> 24 )} 25 {error && ( 26 <Text 27 className="text-sf-red text-xs" 28 accessibilityRole="alert" 29 > 30 {error} 31 </Text> 32 )} 33 </View> 34 ); 35} 36 37function AccessibleForm() { 38 const { bottom } = useSafeAreaInsets(); 39 40 return ( 41 <KeyboardAwareScrollView 42 contentContainerStyle={{ 43 padding: 16, 44 paddingBottom: bottom + 16, 45 gap: 16, 46 }} 47 keyboardShouldPersistTaps="handled" 48 > 49 <FormField label="Email" hint="We'll never share your email"> 50 <TextInput 51 className="bg-sf-fill rounded-xl px-4 py-3 text-sf-text" 52 placeholder="you@example.com" 53 keyboardType="email-address" 54 autoCapitalize="none" 55 autoComplete="email" 56 textContentType="emailAddress" 57 accessibilityLabel="Email address" 58 /> 59 </FormField> 60 61 <FormField label="Password"> 62 <TextInput 63 className="bg-sf-fill rounded-xl px-4 py-3 text-sf-text" 64 placeholder="••••••••" 65 secureTextEntry 66 autoComplete="password" 67 textContentType="password" 68 accessibilityLabel="Password" 69 /> 70 </FormField> 71 </KeyboardAwareScrollView> 72 ); 73}

iOS Liquid Glass Styling

Detecting Liquid Glass Support

tsx
1import { isLiquidGlassAvailable } from "expo-glass-effect"; 2 3const GLASS = isLiquidGlassAvailable(); 4 5const HEADER_OPTIONS = GLASS 6 ? { 7 headerTransparent: true, 8 headerShadowVisible: false, 9 headerBlurEffect: "none", 10 } 11 : { 12 headerTransparent: true, 13 headerBlurEffect: "systemChromeMaterial", 14 headerShadowVisible: true, 15 };

Tab Bar with Glass Effect

tsx
1import { BlurView } from "expo-blur"; 2 3function GlassTabBarBackground() { 4 return ( 5 <BlurView 6 intensity={100} 7 tint="systemChromeMaterial" 8 style={StyleSheet.absoluteFill} 9 /> 10 ); 11} 12 13// Usage in Tabs 14const TAB_OPTIONS = 15 process.env.EXPO_OS === "ios" 16 ? { 17 tabBarBackground: GlassTabBarBackground, 18 tabBarStyle: { position: "absolute" }, 19 } 20 : {};

Glass Card Component

tsx
1import { BlurView } from "expo-blur"; 2import { View } from "@/tw"; 3import { cn } from "@/lib/utils"; 4 5interface GlassCardProps extends React.ComponentProps<typeof View> { 6 intensity?: number; 7} 8 9function GlassCard({ 10 intensity = 50, 11 className, 12 children, 13 ...props 14}: GlassCardProps) { 15 if (process.env.EXPO_OS !== "ios") { 16 // Fallback for non-iOS 17 return ( 18 <View 19 {...props} 20 className={cn( 21 "bg-sf-bg-2/80 rounded-2xl overflow-hidden", 22 className 23 )} 24 > 25 {children} 26 </View> 27 ); 28 } 29 30 return ( 31 <View 32 {...props} 33 className={cn("rounded-2xl overflow-hidden", className)} 34 > 35 <BlurView 36 intensity={intensity} 37 tint="systemChromeMaterial" 38 style={StyleSheet.absoluteFill} 39 /> 40 <View className="relative">{children}</View> 41 </View> 42 ); 43}

Form Components Pattern

The Form compound component demonstrates all principles together:

tsx
1"use client"; 2 3import React, { createContext, use } from "react"; 4import { View, Text, TextInput, ScrollView, TouchableHighlight } from "@/tw"; 5import { useSafeAreaInsets } from "react-native-safe-area-context"; 6import { cn } from "@/lib/utils"; 7import { useCSSVariable } from "@/tw"; 8 9// Context for form styling 10const FormContext = createContext<{ 11 listStyle: "grouped" | "inset"; 12 sheet?: boolean; 13}>({ listStyle: "inset" }); 14 15// List wrapper with pull-to-refresh 16function List({ 17 children, 18 listStyle = "inset", 19 sheet, 20 ...props 21}: React.ComponentProps<typeof ScrollView> & { 22 listStyle?: "grouped" | "inset"; 23 sheet?: boolean; 24}) { 25 const { bottom } = useSafeAreaInsets(); 26 27 return ( 28 <FormContext value={{ listStyle, sheet }}> 29 <ScrollView 30 contentContainerStyle={{ paddingVertical: 16, gap: 24 }} 31 contentInsetAdjustmentBehavior="automatic" 32 scrollIndicatorInsets={{ bottom }} 33 className={cn( 34 sheet ? "bg-transparent" : "bg-sf-grouped-bg", 35 props.className 36 )} 37 {...props} 38 > 39 {children} 40 </ScrollView> 41 </FormContext> 42 ); 43} 44 45// Section groups related items 46function Section({ 47 children, 48 title, 49 footer, 50 ...props 51}: React.ComponentProps<typeof View> & { 52 title?: string; 53 footer?: string; 54}) { 55 const { listStyle, sheet } = use(FormContext); 56 const isInset = listStyle === "inset"; 57 58 return ( 59 <View style={{ paddingHorizontal: isInset ? 16 : 0 }} {...props}> 60 {title && ( 61 <Text className="uppercase text-sf-text-2 text-sm px-5 pb-2"> 62 {title} 63 </Text> 64 )} 65 <View 66 className={cn( 67 sheet ? "bg-sf-bg-2" : "bg-sf-grouped-bg-2", 68 isInset ? "rounded-xl overflow-hidden" : "border-y border-sf-border" 69 )} 70 > 71 {React.Children.map(children, (child, index) => ( 72 <> 73 {child} 74 {index < React.Children.count(children) - 1 && ( 75 <View className="border-b border-sf-border ml-4" /> 76 )} 77 </> 78 ))} 79 </View> 80 {footer && ( 81 <Text className="text-sf-text-2 text-sm px-5 pt-2">{footer}</Text> 82 )} 83 </View> 84 ); 85} 86 87// Individual form item with optional navigation 88function Item({ 89 children, 90 onPress, 91 href, 92 ...props 93}: React.ComponentProps<typeof View> & { 94 onPress?: () => void; 95 href?: string; 96}) { 97 const underlayColor = useCSSVariable("--sf-gray-4"); 98 99 const content = ( 100 <View className="flex-row items-center px-4 py-3 min-h-[44px]" {...props}> 101 {children} 102 </View> 103 ); 104 105 if (!onPress && !href) return content; 106 107 return ( 108 <TouchableHighlight 109 onPress={onPress} 110 underlayColor={underlayColor} 111 className="web:hover:bg-sf-fill web:transition-colors" 112 > 113 {content} 114 </TouchableHighlight> 115 ); 116} 117 118// Text label 119function Label({ className, ...props }: React.ComponentProps<typeof Text>) { 120 return ( 121 <Text 122 {...props} 123 className={cn("text-sf-text text-base flex-1", className)} 124 /> 125 ); 126} 127 128// Hint/value on the right 129function Hint({ className, ...props }: React.ComponentProps<typeof Text>) { 130 return ( 131 <Text 132 {...props} 133 className={cn("text-sf-text-2 text-base", className)} 134 /> 135 ); 136} 137 138// Export compound component 139export const Form = { 140 List, 141 Section, 142 Item, 143 Label, 144 Hint, 145};

Usage

tsx
1<Form.List> 2 <Form.Section title="Account" footer="Your account settings"> 3 <Form.Item href="/profile"> 4 <Form.Label>Profile</Form.Label> 5 <Form.Hint>John Doe</Form.Hint> 6 <ChevronRight /> 7 </Form.Item> 8 <Form.Item href="/email"> 9 <Form.Label>Email</Form.Label> 10 <Form.Hint>john@example.com</Form.Hint> 11 <ChevronRight /> 12 </Form.Item> 13 </Form.Section> 14 15 <Form.Section title="Preferences"> 16 <Form.Item> 17 <Form.Label>Dark Mode</Form.Label> 18 <Switch value={darkMode} onValueChange={setDarkMode} /> 19 </Form.Item> 20 </Form.Section> 21</Form.List>

Haptic Feedback

Platform-Safe Haptics

lib/haptics.ts (native):

tsx
1import * as Haptics from "expo-haptics"; 2 3export const haptics = { 4 light: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light), 5 medium: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium), 6 heavy: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy), 7 success: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success), 8 warning: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning), 9 error: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error), 10 selection: () => Haptics.selectionAsync(), 11};

lib/haptics.web.ts (web - no-op):

tsx
1export const haptics = { 2 light: () => {}, 3 medium: () => {}, 4 heavy: () => {}, 5 success: () => {}, 6 warning: () => {}, 7 error: () => {}, 8 selection: () => {}, 9};

Usage in Components

tsx
1import { haptics } from "@/lib/haptics"; 2 3function HapticButton({ onPress, children }) { 4 const handlePress = () => { 5 haptics.light(); 6 onPress?.(); 7 }; 8 9 return <Pressable onPress={handlePress}>{children}</Pressable>; 10}

Icon System

SF Symbol Icons with Fallbacks

tsx
1import { SymbolView, SymbolWeight } from "expo-symbols"; 2import { MaterialIcons } from "@expo/vector-icons"; 3 4// Map SF Symbol names to Material Icons 5const ICON_MAPPING: Record<string, string> = { 6 "house.fill": "home", 7 "gear": "settings", 8 "person.fill": "person", 9 "magnifyingglass": "search", 10 "chevron.right": "chevron_right", 11}; 12 13interface IconProps { 14 name: string; 15 size?: number; 16 color?: string; 17 weight?: SymbolWeight; 18} 19 20export function Icon({ name, size = 24, color, weight }: IconProps) { 21 if (process.env.EXPO_OS === "ios") { 22 return ( 23 <SymbolView 24 name={name} 25 size={size} 26 tintColor={color} 27 weight={weight} 28 /> 29 ); 30 } 31 32 const materialName = ICON_MAPPING[name] || name; 33 return <MaterialIcons name={materialName} size={size} color={color} />; 34}

Component Checklist

When creating a new component, ensure:

  • Portable: Self-contained, minimal external dependencies
  • Typed: Full TypeScript types for props
  • Themed: Uses CSS variables for colors, not hardcoded values
  • Accessible: Proper accessibility roles and labels
  • Keyboard-aware: Handles keyboard avoidance for inputs
  • Safe area-aware: Respects device safe areas
  • Platform-adaptive: Uses native primitives where available
  • Compound structure: Complex components use compound pattern
  • Haptic feedback: Provides tactile feedback on iOS
  • Dark mode: Supports light and dark color schemes
  • Display name: Set displayName in dev for debugging
tsx
1if (__DEV__) { 2 MyComponent.displayName = "MyComponent"; 3}

Dependencies Reference

PackagePurpose
react-native-cssCSS runtime for React Native
nativewindMetro transformer for Tailwind
tailwindcssUtility-first CSS
@tailwindcss/postcssPostCSS plugin for Tailwind v4
tailwind-mergeMerge Tailwind classes safely
clsxConditional class names
react-native-safe-area-contextSafe area handling
react-native-keyboard-controllerKeyboard animations
react-native-reanimatedGesture animations
expo-hapticsHaptic feedback
expo-symbolsSF Symbols
expo-blurBlur effects
expo-glass-effectiOS 26 liquid glass
@bacons/apple-colorsNative iOS colors

FAQ & Installation Steps

These questions and steps mirror the structured data on this page for better search understanding.

? Frequently Asked Questions

What is reusable-ui-components?

Perfect for Frontend Agents needing portable and native-first UI components for Expo Router apps Free UI components I use for building Expo Router apps

How do I install reusable-ui-components?

Run the command: npx killer-skills add EvanBacon/crispy/reusable-ui-components. It works with Cursor, Windsurf, VS Code, Claude Code, and 19+ other IDEs.

What are the use cases for reusable-ui-components?

Key use cases include: Building reusable UI components for Expo Router apps, Creating native-first and portable UI elements with liquid glass aesthetics, Designing UI components that follow iOS San Francisco design guidelines.

Which IDEs are compatible with reusable-ui-components?

This skill is compatible with Cursor, Windsurf, VS Code, Trae, Claude Code, OpenClaw, Aider, Codex, OpenCode, Goose, Cline, Roo Code, Kiro, Augment Code, Continue, GitHub Copilot, Sourcegraph Cody, and Amazon Q Developer. Use the Killer-Skills CLI for universal one-command installation.

Are there any limitations for reusable-ui-components?

Requires Expo Router for app development. Limited to iOS San Francisco design guidelines and liquid glass aesthetics.

How To Install

  1. 1. Open your terminal

    Open the terminal or command line in your project directory.

  2. 2. Run the install command

    Run: npx killer-skills add EvanBacon/crispy/reusable-ui-components. The CLI will automatically detect your IDE or AI agent and configure the skill.

  3. 3. Start using the skill

    The skill is now active. Your AI agent can use reusable-ui-components immediately in the current project.

Related Skills

Looking for an alternative to reusable-ui-components or another community skill for your workflow? Explore these related open-source skills.

View All

widget-generator

Logo of f
f

f.k.a. Awesome ChatGPT Prompts. Share, discover, and collect prompts from the community. Free and open source — self-host for your organization with complete privacy.

149.6k
0
AI

flags

Logo of vercel
vercel

flags is a Next.js feature management skill that enables developers to efficiently add or modify framework feature flags, streamlining React application development.

138.4k
0
Browser

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
AI

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
AI