Styling Rules
RNCopilot uses react-native-unistyles 3.x for all styling. This page covers the three fundamental laws, the variants API, responsive helpers, and how to use theme tokens.
The Three Laws
These are non-negotiable. Every style in the codebase follows them.
- Never use inline styles. All styles go in
StyleSheet.create. - Never use color literals. All colors come from
theme.colors.*. - Never hardcode numeric spacing. Use
theme.metrics.spacing.*or the responsive helpers.
// WRONG -- all three laws violated
<View style={{ padding: 16, backgroundColor: '#fff' }}>
// CORRECT
<View style={styles.container}>StyleSheet Pattern
Import StyleSheet from react-native-unistyles (not from react-native). The callback receives the active theme, giving you full access to tokens:
import { StyleSheet } from 'react-native-unistyles';
const styles = StyleSheet.create((theme) => ({
container: {
padding: theme.metrics.spacing.p16,
backgroundColor: theme.colors.background.surface,
borderRadius: theme.metrics.borderRadius.md,
},
}));Separate Style Files
When styles are substantial, put them in a dedicated .styles.ts file:
// Button.styles.ts
import { StyleSheet } from 'react-native-unistyles';
export const styles = StyleSheet.create((theme) => ({
container: {
borderRadius: theme.metrics.borderRadius.lg,
alignItems: 'center',
justifyContent: 'center',
},
}));Variants API
Use the unistyles variants API for any style that changes based on props. Define variants declaratively inside the style object:
// Component.styles.ts
import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles';
export const styles = StyleSheet.create((theme) => ({
container: {
borderRadius: theme.metrics.borderRadius.lg,
variants: {
variant: {
primary: { backgroundColor: theme.colors.brand.primary },
secondary: { backgroundColor: theme.colors.background.surfaceAlt },
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.border.default,
},
},
size: {
sm: { padding: theme.metrics.spacing.p8 },
md: { padding: theme.metrics.spacing.p12 },
lg: { padding: theme.metrics.spacing.p16 },
},
disabled: {
true: { opacity: 0.5 },
},
},
},
}));
export type MyComponentStyleVariants = UnistylesVariants<typeof styles>;In the component, call useVariants once before accessing any styles:
// Component.tsx
import { styles } from './Component.styles';
export function MyComponent({ variant, size, disabled }: MyComponentProps) {
styles.useVariants({ variant, size, disabled });
return <View style={styles.container} />;
}Call styles.useVariants(...) once per render, before accessing any style property. Do not call it conditionally or inside loops.
Key Variant Rules
- Use
compoundVariantsfor styles that depend on multiple variant combinations. - Use boolean variants (
true/falsekeys) for toggleable states likedisabled,focused,error. - Use breakpoint-responsive values for tablet adaptation:
padding: { xs: theme.metrics.spacing.p12, md: theme.metrics.spacing.p24 }miniRuntime
The StyleSheet.create callback receives a second argument (rt) for safe area insets and screen dimensions:
const styles = StyleSheet.create((theme, rt) => ({
container: {
paddingTop: rt.insets.top,
paddingBottom: rt.insets.bottom,
},
}));Responsive Helpers
Import from @/theme/metrics:
import { rf, hs, vs } from '@/theme/metrics';| Helper | Purpose | Scales With |
|---|---|---|
rf(n) | Responsive font size | Screen width (base: 390) |
hs(n) | Horizontal scale | Screen width (base: 390) |
vs(n) | Vertical scale | Screen height (base: 844) |
Use these in StyleSheet.create for dynamic values not available as theme tokens:
const styles = StyleSheet.create((theme) => ({
customElement: {
width: hs(120),
height: vs(48),
fontSize: rf(14),
},
}));The theme already uses hs(), vs(), and rf() internally for all spacing, font size, and border radius tokens. Prefer using theme.metrics.* tokens when a matching value exists.
Theme Token Quick Reference
For a full listing of every token and its value, see the Theme Tokens reference page. Here is a summary of the categories available on the theme object:
theme.colors.brand.* brand colors (primary, secondary, tertiary, variants)
theme.colors.background.* surface colors (app, surface, surfaceAlt, elevated, ...)
theme.colors.text.* text colors (primary, secondary, inverse, link, ...)
theme.colors.border.* border colors (default, subtle, strong, focus, ...)
theme.colors.icon.* icon colors
theme.colors.state.* semantic colors (success, warning, error, info + backgrounds)
theme.colors.overlay.* overlay and shadow colors
theme.colors.gradient.* gradient arrays
theme.colors.shadow.* shadow properties
theme.colors.mode 'light' | 'dark'
theme.metrics.spacing.* horizontal spacing (p4 through p120, scaled via hs())
theme.metrics.spacingV.* vertical spacing (p4 through p120, scaled via vs())
theme.metrics.fontSize.* font sizes (xxs through 6xl, scaled via rf())
theme.metrics.borderRadius.* border radii (xs, sm, md, lg, xl, full)
theme.metrics.iconSize.* icon sizes (xs, sm, md, lg, xl)