
* Add style prop to UserAvatar component for better customization * Refactor UserAvatar to use getAvatarSeed utility for consistent avatar generation * Fix React hook ordering issues in profile/overview.tsx to prevent crashes during auth state changes * Add proper state initialization and cleanup during authentication transitions * Ensure consistent fallback avatar display for unauthenticated users These changes improve stability during login/logout operations and provide better visual continuity with Robohash avatars when profile images aren't available.
21 KiB
POWR App Styling Guide
Last Updated: 2025-04-01
Status: Active
Related To: Design System, Component Architecture, UI/UX, Cross-Platform Development
Purpose
This document outlines the styling principles, component usage patterns, and theming implementation for the POWR fitness app. Following these guidelines ensures a consistent look and feel across the application, facilitates cross-platform compatibility, and enhances the overall user experience.
Cross-Platform Approach
The POWR app is designed to run on both iOS and Android platforms, which present unique challenges for UI implementation. Our approach prioritizes:
- Platform Consistency: Maintaining a consistent look and feel across platforms
- Platform Adaptation: Respecting platform-specific UX patterns where appropriate
- Graceful Fallbacks: Implementing fallbacks for features not available on all platforms
- Testing on Both Platforms: All UI changes must be verified on both iOS and Android
Theme System Architecture
The POWR app uses a flexible theme system built with React Native, Tailwind CSS, and shadcn/ui components. The theming infrastructure supports both light and dark modes, with dynamic color adjustments for different UI states.
Theme File Organization
lib/theme/
├── index.ts - Main theme export
├── colors.ts - Color definitions
├── constants.ts - Theme constants
├── iconUtils.ts - Icon styling utilities
└── useColorScheme.tsx - Theme mode selection hook
Theme Implementation Strategy
The application uses:
- Tailwind classes for general styling with
nativewind
- Specialized hooks for cross-platform compatibility (
useIconColor
, etc.) shadcn/ui
component library for consistent UI elements
Color System
All colors should be accessed through the theme system rather than using hardcoded values. Never use direct color codes in components.
Color Imports
// Import theme utilities
import { useTheme } from '@/lib/theme';
import { useIconColor } from '@/lib/theme/iconUtils';
import { FIXED_COLORS } from '@/lib/theme/colors';
Color Variants
The theme includes semantic color variants for different UI elements:
primary
- Brand color, used for main interactive elements (purple)secondary
- Supporting UI elementsmuted
- Subdued elements, backgrounds, disabled statesaccent
- Highlights and accentsdestructive
- Error states, deletion actions (red)success
- Confirmation, completion states (green)warning
- Caution states (yellow/orange)
Accessing Colors
Always access colors through Tailwind classes:
// Good - uses theme system
<View className="bg-primary rounded-md p-4">
<Text className="text-primary-foreground font-medium">
Hello World
</Text>
</View>
// Bad - hardcoded values that won't respond to theme changes
<View style={{ backgroundColor: '#8B5CF6', borderRadius: 8, padding: 16 }}>
<Text style={{ color: '#FFFFFF', fontWeight: 500 }}>
Hello World
</Text>
</View>
Icon Styling
Icons must use the icon utility functions to ensure visibility across platforms. Different platforms may require different stroke widths, colors, and other properties.
Icon Usage
import { useIconColor } from '@/lib/theme/iconUtils';
import { Play, Star, Trash2 } from 'lucide-react-native';
// Inside your functional component
function MyComponent() {
const { getIconProps, getIconColor } = useIconColor();
return (
<View>
{/* Primary action icon */}
<Play {...getIconProps('primary')} size={20} />
{/* Destructive action icon */}
<Trash2 {...getIconProps('destructive')} size={20} />
{/* Icon with conditional fill */}
<Star
{...getIconProps(isFavorite ? 'primary' : 'muted')}
fill={isFavorite ? getIconColor('primary') : "none"}
size={20}
/>
</View>
);
}
Icon Variants
primary
- For main actions and interactive elementsmuted
- For secondary or less important actionsdestructive
- For delete/remove actionssuccess
- For confirmation/complete actionswarning
- For caution indicators
Platform-Specific Icon Considerations
-
Android:
- Icons often appear thinner and less visible on Android
- Always use
strokeWidth={2}
or higher for better visibility on Android - Minimum recommended icon size is 24px for Android (vs. 20px for iOS)
- Use the
getIconProps
function which handles these platform differences automatically
-
iOS:
- Icons generally appear as expected with default stroke width
- iOS has better support for gradients and complex icon styles
Button Styling
Use the standard Button
component with appropriate variants to maintain a consistent look and feel.
Button Variants
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
// Primary button
<Button variant="default" className="w-full">
<Text className="text-primary-foreground">Primary Action</Text>
</Button>
// Destructive button
<Button variant="destructive" className="w-full">
<Text className="text-destructive-foreground">Delete</Text>
</Button>
// Outline button
<Button variant="outline" className="w-full">
<Text>Secondary Action</Text>
</Button>
// Ghost button (minimal visual impact)
<Button variant="ghost" className="w-full">
<Text>Subtle Action</Text>
</Button>
// Link button
<Button variant="link" className="w-full">
<Text className="text-primary underline">Learn More</Text>
</Button>
Button States
Buttons handle the following states automatically through the theme system:
- Default
- Hover/active (handled differently on mobile and web)
- Disabled
- Loading
// Disabled button
<Button variant="default" disabled className="w-full">
<Text className="text-primary-foreground">Unavailable</Text>
</Button>
// Loading button
<Button variant="default" isLoading className="w-full">
<Text className="text-primary-foreground">Loading...</Text>
</Button>
Platform-Specific Button Considerations
-
Android:
- Android buttons may need additional padding to match iOS visual weight
- Use
android:elevation
or equivalent shadow values for proper elevation on Android - Ripple effects require additional configuration to work properly
- Consider using
TouchableNativeFeedback
for Android-specific feedback on buttons
-
iOS:
- iOS buttons typically have more subtle feedback effects
- Shadow properties work more predictably on iOS
Header Component
Use the Header
component consistently across all screens for navigation and context.
Header Configuration
import { Header } from '@/components/Header';
// Standard header with title
<Header title="Screen Title" showNotifications={true} />
// Header with logo
<Header useLogo={true} showNotifications={true} />
// Header with custom right element
<Header
title="Screen Title"
rightElement={<YourCustomElement />}
/>
// Header with back button
<Header
title="Details"
showBackButton={true}
onBack={() => navigation.goBack()}
/>
Platform-Specific Header Considerations
-
Android:
- Android status bar customization requires
StatusBar
component with platform checks - Text in headers may render differently, requiring platform-specific adjustments
- Back button styling differs between platforms - use the Header component's built-in options
- Shadow effects need to be handled differently on Android (elevation vs shadowProps)
- Android status bar customization requires
-
iOS:
- iOS has native support for large titles and collapsible headers
- Safe area insets are critical for proper header positioning on iOS
- Status bar content color changes (dark/light) may need to be explicitly specified
Text Styling
Use the Text
component with appropriate Tailwind classes for typography. This ensures the correct font styles across platforms.
Text Hierarchy
import { Text } from '@/components/ui/text';
// Page title
<Text className="text-2xl font-bold text-foreground">
Page Title
</Text>
// Section heading
<Text className="text-xl font-semibold text-foreground mb-2">
Section Heading
</Text>
// Subsection heading
<Text className="text-lg font-medium text-foreground mb-1">
Subsection Heading
</Text>
// Body text
<Text className="text-base text-foreground">
Regular body text for primary content.
</Text>
// Secondary text
<Text className="text-sm text-muted-foreground">
Secondary information or supporting text.
</Text>
// Small text / captions
<Text className="text-xs text-muted-foreground">
Caption text, timestamps, etc.
</Text>
Platform-Specific Text Considerations
-
Android:
- Font rendering is different on Android - text may appear smaller or thinner
- Android requires explicit
fontFamily
specification for custom fonts - Line height calculations differ between platforms - may need adjustments
- Some text styling properties like
letterSpacing
work differently on Android - Use
includeFontPadding: false
on Android to fix inconsistent text height
-
iOS:
- Dynamic Type (iOS accessibility feature) should be supported
- Certain text styles like small caps require different implementations
- Font weights map differently between platforms (400 on iOS may not look the same as 400 on Android)
Card Components
Use the Card component family for content blocks throughout the app.
Basic Card
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
<Card className="mx-4 mb-4">
<CardHeader>
<CardTitle>
<Text className="text-lg font-semibold">Card Title</Text>
</CardTitle>
</CardHeader>
<CardContent className="p-4">
<Text className="text-foreground">
Card content goes here.
</Text>
</CardContent>
<CardFooter className="flex-row justify-between px-4 py-2">
<Button variant="ghost" size="sm">
<Text>Cancel</Text>
</Button>
<Button variant="default" size="sm">
<Text className="text-primary-foreground">Confirm</Text>
</Button>
</CardFooter>
</Card>
Interactive Card
For cards that function as buttons:
<Pressable onPress={handlePress}>
<Card className="mx-4 mb-4 border-l-4 border-l-primary">
<CardContent className="p-4">
<Text className="text-foreground font-medium">
Interactive Card
</Text>
<Text className="text-sm text-muted-foreground mt-1">
Tap to interact
</Text>
</CardContent>
</Card>
</Pressable>
Platform-Specific Card Considerations
-
Android:
- Use
elevation
for shadows on Android instead ofshadow-*
classes - Border radius may render differently on older Android versions
- Ripple effects for interactive cards need platform-specific configuration
- Border styles may appear differently on Android
- Use
-
iOS:
- Shadow properties work more predictably on iOS
- Cards with dynamic height may need additional configuration for iOS
Dialog/Alert Styling
Center buttons in dialogs for better usability and maintain consistent styling for these components.
Alert Dialog Example
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
<AlertDialog>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Text className="text-lg font-semibold text-foreground">
Confirm Action
</Text>
</AlertDialogTitle>
<AlertDialogDescription>
<Text className="text-muted-foreground">
Are you sure you want to continue? This action cannot be undone.
</Text>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-row justify-center gap-3 mt-4">
<AlertDialogCancel>
<Text>Cancel</Text>
</AlertDialogCancel>
<AlertDialogAction className="bg-destructive">
<Text className="text-destructive-foreground">Confirm</Text>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Platform-Specific Dialog Considerations
-
Android:
- Android dialogs traditionally have buttons aligned to the right
- Back button behavior needs special handling on Android dialogs
- Touch outside to dismiss works differently on Android
- Dialog animations differ between platforms
- Material Design guidelines suggest different spacing and typography than iOS
-
iOS:
- iOS dialogs typically have vertically stacked buttons
- Safe area insets must be respected on full-screen iOS sheets
- iOS has specific swipe gestures for sheet dismissal
Form Elements
Style form elements consistently for a coherent user experience.
Form Field Examples
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
// Text input with label
<View className="mb-4">
<Label htmlFor="name" className="mb-1.5">
<Text className="text-sm font-medium">Name</Text>
</Label>
<Input
id="name"
placeholder="Enter your name"
value={name}
onChangeText={setName}
className="bg-background"
/>
{error && (
<Text className="text-xs text-destructive mt-1">
{error}
</Text>
)}
</View>
// Select input with label
<View className="mb-4">
<Label htmlFor="category" className="mb-1.5">
<Text className="text-sm font-medium">Category</Text>
</Label>
<Select
id="category"
value={category}
onValueChange={setCategory}
className="bg-background"
>
{categories.map(cat => (
<SelectItem key={cat.id} label={cat.name} value={cat.id} />
))}
</Select>
</View>
Platform-Specific Form Element Considerations
-
Android:
- Input fields may need additional padding or height adjustments
- Text field focus appearance differs significantly (Material Design vs. iOS)
- Android requires explicit configuration for soft keyboard behavior
- Date/time pickers have completely different UIs between platforms
- Dropdown selects appear and behave differently on Android
-
iOS:
- Form elements typically have a lighter visual style
- iOS has specific picker components that are different from Android
- Keyboard accessories are common on iOS but less so on Android
- Text selection handles and behavior differ between platforms
Common Cross-Platform Issues and Solutions
Shadow Implementation
Issue: Shadow styling works differently between iOS and Android.
Solution:
// Cross-platform shadow solution
<View
className="bg-card rounded-lg p-4"
style={Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
})}
>
<Text>Content with consistent shadow across platforms</Text>
</View>
Icon Rendering
Issue: Icons appear properly on iOS but are barely visible on Android.
Solution:
// Always use the icon utility
import { useIconColor } from '@/lib/theme/iconUtils';
function MyComponent() {
const { getIconProps } = useIconColor();
return (
<Icon
{...getIconProps('primary')}
size={24} // Slightly larger for Android
strokeWidth={Platform.OS === 'android' ? 2 : 1.5} // Explicit adjustment
/>
);
}
Text Alignment
Issue: Text alignment and truncation behaves differently across platforms.
Solution:
// Text alignment helper component
function AlignedText({ children, ...props }) {
return (
<Text
{...props}
style={[
props.style,
Platform.OS === 'android' ? { includeFontPadding: false } : null,
Platform.OS === 'android' ? { lineHeight: 24 } : null,
]}
>
{children}
</Text>
);
}
Touchable Feedback
Issue: Touch feedback effects differ between platforms.
Solution:
// Platform-specific touchable
function AppTouchable({ children, onPress, ...props }) {
if (Platform.OS === 'android') {
return (
<TouchableNativeFeedback
onPress={onPress}
background={TouchableNativeFeedback.Ripple('#rgba(0,0,0,0.1)', false)}
{...props}
>
<View>{children}</View>
</TouchableNativeFeedback>
);
}
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.7} {...props}>
{children}
</TouchableOpacity>
);
}
Keyboard Handling
Issue: Keyboard behavior and avoidance differs between platforms.
Solution:
// Keyboard handling helper
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
style={{ flex: 1 }}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={{ flex: 1 }}>
{/* Form content */}
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
Platform-Specific Component Extensions
For cases where significant platform differences exist, create platform-specific component extensions:
Example: Platform-Specific DatePicker
// DatePickerWrapper.jsx
import { DatePicker } from './DatePicker.ios';
import { DatePicker } from './DatePicker.android';
export const DatePickerWrapper = (props) => {
const Component = Platform.select({
ios: DatePickerIOS,
android: DatePickerAndroid,
});
return <Component {...props} />;
};
Best Practices for Cross-Platform Development
- Always test on both platforms before considering a feature complete
- Use platform detection judiciously - prefer cross-platform solutions where possible
- Create abstraction layers for significantly different platform components
- Leverage UI component libraries that handle cross-platform differences (like UI Kitten, React Native Paper)
- Document platform-specific quirks that you encounter for future reference
- Create utility functions for common platform-specific adjustments
- Use feature detection instead of platform detection when possible
- Consider native device capabilities like haptic feedback that may not exist on all devices
Best Practices for POWR App Styling
- Never use hardcoded colors - Always use theme variables through Tailwind classes
- Always use
getIconProps
for icons - Ensures visibility on both iOS and Android - Use semantic variants - Choose button and icon variants based on their purpose
- Maintain consistent spacing - Use Tailwind spacing classes (p-4, m-2, etc.)
- Test both platforms - Verify UI rendering on both iOS and Android
- Use platform-specific overrides when necessary
- Document platform-specific behavior in component comments
Troubleshooting Common Issues
Icons Not Visible on Android
Problem: Icons don't appear or are difficult to see on Android devices.
Solution:
- Ensure you're using
getIconProps()
instead of direct styling - Add
strokeWidth={2}
to increase visibility - Verify that icon size is appropriate (min 24px recommended for Android)
- Check that the icon color has sufficient contrast with the background
Inconsistent Colors
Problem: Colors appear inconsistent between components or platforms.
Solution:
- Verify you're using Tailwind classes (text-primary vs #8B5CF6)
- Check that the correct variant is being used for the component
- Ensure components are properly wrapped with theme provider
- Examine component hierarchy for style inheritance issues
Text Truncation Issues
Problem: Text doesn't truncate properly or layout breaks with long content.
Solution:
- Add
numberOfLines={1}
for single-line truncation - Use
ellipsizeMode="tail"
for text truncation - Wrap Text components with a fixed-width container
- Consider using a more robust solution for responsive text
- Apply platform-specific text style adjustments
Shadow and Elevation
Problem: Shadows appear on iOS but not on Android, or look inconsistent.
Solution:
- Use platform-specific shadow implementation (see example above)
- For Android, use
elevation
property - For iOS, use
shadowColor
,shadowOffset
,shadowOpacity
, andshadowRadius
- Test shadow values on different Android versions
Keyboard Issues
Problem: Keyboard covers input fields or doesn't dismiss properly.
Solution:
- Use KeyboardAvoidingView with platform-specific behavior
- Implement Keyboard.dismiss on background taps
- Add ScrollView for forms to ensure all fields are accessible
- Consider using a keyboard manager library for complex forms
Related Documentation
- Coding Style Guide - General coding patterns and practices
- Component Architecture - How components are organized
- Accessibility Guidelines - Making the app accessible to all users