Skip to Content
Core ConceptsStyling with Unistyles

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

  1. No inline styles. All styles go in StyleSheet.create.
  2. No color literals. All colors come from theme.colors.*.
  3. No hardcoded spacing. Use theme.metrics.spacing.*, theme.metrics.spacingV.*, or the hs()/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:

PropertyTypeDescription
rt.insets.topnumberStatus bar safe area
rt.insets.bottomnumberHome indicator safe area
rt.insets.leftnumberLeft safe area (landscape)
rt.insets.rightnumberRight safe area (landscape)
rt.screen.widthnumberScreen width
rt.screen.heightnumberScreen 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:

BreakpointMin Width
xs0px
sm576px
md768px
lg992px
xl1200px

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.ts

The 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.

Last updated on