Styling with Unistyles
RNCopilot uses react-native-unistyles 3.x for all styling. This replaces the standard React Native StyleSheet with a theme-aware version that supports variants, breakpoints, and runtime theming.
Critical: Always import StyleSheet from 'react-native-unistyles', never from 'react-native'. The unistyles version receives the theme callback.
The Three Laws of Styling
- No inline styles. All styles go in
StyleSheet.create. - No color literals. All colors come from
theme.colors.*. - No hardcoded spacing. Use
theme.metrics.spacing.*,theme.metrics.spacingV.*, or thehs()/vs()helpers.
Basic Pattern
Every stylesheet receives the current theme as a callback parameter:
import { StyleSheet } from 'react-native-unistyles';
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
backgroundColor: theme.colors.background.app,
padding: theme.metrics.spacing.p16,
},
title: {
fontSize: theme.fonts.size.xl,
color: theme.colors.text.primary,
fontFamily: 'Inter-Bold',
},
subtitle: {
fontSize: theme.fonts.size.sm,
color: theme.colors.text.secondary,
},
}));When the theme changes (e.g., toggling dark mode), all stylesheets automatically recalculate. You do not need to manage theme subscriptions manually.
Variants API
The variants API lets you define style variations declaratively inside StyleSheet.create. This is the preferred way to handle prop-driven style changes.
Defining Variants
// Button.styles.ts
import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles';
export const styles = StyleSheet.create((theme) => ({
container: {
borderRadius: theme.metrics.borderRadius.lg,
alignItems: 'center',
justifyContent: 'center',
variants: {
variant: {
primary: {
backgroundColor: theme.colors.brand.primary,
},
secondary: {
backgroundColor: theme.colors.background.surfaceAlt,
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.border.default,
},
ghost: {
backgroundColor: 'transparent',
},
},
size: {
sm: {
paddingVertical: theme.metrics.spacingV.p8,
paddingHorizontal: theme.metrics.spacing.p12,
},
md: {
paddingVertical: theme.metrics.spacingV.p12,
paddingHorizontal: theme.metrics.spacing.p16,
},
lg: {
paddingVertical: theme.metrics.spacingV.p16,
paddingHorizontal: theme.metrics.spacing.p24,
},
},
},
},
label: {
fontFamily: 'Inter-SemiBold',
variants: {
variant: {
primary: { color: theme.colors.text.inverse },
secondary: { color: theme.colors.text.primary },
outline: { color: theme.colors.text.primary },
ghost: { color: theme.colors.brand.primary },
},
size: {
sm: { fontSize: theme.fonts.size.sm },
md: { fontSize: theme.fonts.size.md },
lg: { fontSize: theme.fonts.size.lg },
},
},
},
}));
export type ButtonStyleVariants = UnistylesVariants<typeof styles>;Using Variants in Components
Call styles.useVariants() once at the top of your component, then use the style objects normally:
// Button.tsx
import { Pressable, Text } from 'react-native';
import { styles } from './Button.styles';
import type { ButtonProps } from './Button.types';
export function Button({ variant = 'primary', size = 'md', title, onPress }: ButtonProps) {
// Activate variants for this render
styles.useVariants({ variant, size });
return (
<Pressable style={styles.container} onPress={onPress}>
<Text style={styles.label}>{title}</Text>
</Pressable>
);
}Compound Variants
Use compoundVariants when a style depends on a specific combination of variant values:
const styles = StyleSheet.create((theme) => ({
container: {
variants: {
variant: {
primary: { backgroundColor: theme.colors.brand.primary },
outline: { backgroundColor: 'transparent', borderWidth: 1 },
},
size: {
sm: { padding: theme.metrics.spacing.p8 },
lg: { padding: theme.metrics.spacing.p16 },
},
},
compoundVariants: [
{
variant: 'outline',
size: 'lg',
styles: {
borderWidth: 2, // thicker border for large outline buttons
borderColor: theme.colors.brand.primary,
},
},
],
},
}));Boolean Variants
For toggleable states like disabled, focused, or error, use boolean keys (true / false):
const styles = StyleSheet.create((theme) => ({
container: {
variants: {
disabled: {
true: { opacity: 0.5 },
false: { opacity: 1 },
},
focused: {
true: {
borderColor: theme.colors.border.focus,
borderWidth: 2,
},
},
error: {
true: {
borderColor: theme.colors.state.error,
},
},
},
},
}));styles.useVariants({ disabled, focused, error: !!errorMessage });miniRuntime (Safe Area Insets)
The second argument to the StyleSheet.create callback is the miniRuntime (rt), which provides safe area insets, screen dimensions, and other runtime values:
const styles = StyleSheet.create((theme, rt) => ({
header: {
paddingTop: rt.insets.top,
backgroundColor: theme.colors.background.surface,
},
footer: {
paddingBottom: rt.insets.bottom,
},
fullScreen: {
width: rt.screen.width,
height: rt.screen.height,
},
}));Available properties on rt:
| Property | Type | Description |
|---|---|---|
rt.insets.top | number | Status bar safe area |
rt.insets.bottom | number | Home indicator safe area |
rt.insets.left | number | Left safe area (landscape) |
rt.insets.right | number | Right safe area (landscape) |
rt.screen.width | number | Screen width |
rt.screen.height | number | Screen height |
Breakpoint-Responsive Values
For tablet adaptation, you can specify different values per breakpoint:
const styles = StyleSheet.create((theme) => ({
container: {
padding: {
xs: theme.metrics.spacing.p12,
md: theme.metrics.spacing.p24,
xl: theme.metrics.spacing.p32,
},
flexDirection: {
xs: 'column',
md: 'row',
},
},
}));The configured breakpoints are:
| Breakpoint | Min Width |
|---|---|
xs | 0px |
sm | 576px |
md | 768px |
lg | 992px |
xl | 1200px |
Separate Style Files
For non-trivial components, styles live in a dedicated .styles.ts file:
Button/
Button.tsx
Button.styles.ts <-- styles here
Button.types.ts
index.tsThe component imports and uses the styles:
// Button.tsx
import { styles } from './Button.styles';For simple components or screens, inline StyleSheet.create at the bottom of the file is acceptable:
export default function HomeScreen() {
return <View style={styles.container} />;
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
backgroundColor: theme.colors.background.app,
},
}));Exporting Variant Types
Export the variant type from your styles file so the component’s props interface can extend it:
// MyComponent.styles.ts
import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles';
export const styles = StyleSheet.create((theme) => ({
container: {
variants: {
size: {
sm: { /* ... */ },
md: { /* ... */ },
lg: { /* ... */ },
},
},
},
}));
export type MyComponentStyleVariants = UnistylesVariants<typeof styles>;// MyComponent.types.ts
import type { ViewProps } from 'react-native';
import type { MyComponentStyleVariants } from './MyComponent.styles';
export interface MyComponentProps extends ViewProps, MyComponentStyleVariants {
title: string;
onPress?: () => void;
}Common Mistakes
Wrong import: import { StyleSheet } from 'react-native'
Correct: import { StyleSheet } from 'react-native-unistyles'
Wrong pattern: StyleSheet.create({ container: { ... } })
Correct: StyleSheet.create((theme) => ({ container: { ... } }))
The theme callback is required for accessing tokens.
Wrong: style={{ padding: 16, color: '#6366F1' }}
Correct: Use theme.metrics.spacing.p16 and theme.colors.brand.primary inside StyleSheet.create.