Skip to main content

Custom Theme

Theme shape

theme.ts should be parser-friendly: define only token data and avoid importing react-native runtime APIs. Handle dynamic values in AppThemeProvider and pass completed values into createTheme().

danger

If theme.ts imports runtime modules (react-native, react, hooks, providers, app services), CLI theme generation can fail (generate-theme-type fails to bundle the theme file). Keep theme files as static token definitions.

A theme is a plain object matching ThemedDict:

type ThemedDict = {
space: Record<string, SpaceValue>;
sizes: Record<string, SizesValue>;
colors: Record<string, ColorsValue>;
radii: Record<string, RadiiValue>;
typography: Record<string, TypographyValue>;
breakpoints?: number[];
};

Use createTheme to build a theme from scratch or extend an existing one.

Overriding tokens

createTheme(base, overrides) shallow-merges each token group. breakpoints is replaced, not merged.

import { createTheme, defaultTheme } from '@react-native-styled-system/util';

const theme = createTheme(defaultTheme, {
colors: { brand: '#FF6600' },
radii: { card: 20 },
});

Semantic colors with createThemeColors

createThemeColors generates light and dark semantic color sets following the shadcn/ui convention.

import { createThemeColors } from '@react-native-styled-system/util';

const { light, dark } = createThemeColors({
base: 'zinc', // gray scale: 'slate' | 'gray' | 'zinc' | 'neutral' | 'stone'
theme: 'blue', // any Tailwind color name
});

Both light and dark share the same keys:

TokenDescription
background / foregroundPage background and default text
card / card-foregroundCard surfaces
primary / primary-foregroundPrimary actions and buttons
secondary / secondary-foregroundSecondary surfaces
muted / muted-foregroundSubdued backgrounds and helper text
accent / accent-foregroundHighlighted content areas
destructive / destructive-foregroundError states and dangerous actions
popover / popover-foregroundPopover surfaces
border / input / ringBorders, input outlines, focus rings
chart-1 ~ chart-5Chart colors

Dark mode

Since light and dark share the same keys, toggling dark mode is just swapping the active color set in your provider.

AppThemeProvider.tsx
import React, { useContext, useMemo, useState } from 'react';
import { StyledSystemProvider } from '@react-native-styled-system/core';
import { createTheme, createThemeColors, defaultTheme } from '@react-native-styled-system/util';

const { light, dark } = createThemeColors({ base: 'neutral', theme: 'blue' });

const DarkModeContext = React.createContext({
isDarkMode: false,
toggleDarkMode: () => {},
});

export const useDarkMode = () => useContext(DarkModeContext);

export const AppThemeProvider = ({ children }: React.PropsWithChildren) => {
const [isDarkMode, setDarkMode] = useState(false);

const theme = useMemo(
() => createTheme(defaultTheme, { colors: isDarkMode ? dark : light }),
[isDarkMode],
);

return (
<DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode: () => setDarkMode((v) => !v) }}>
<StyledSystemProvider theme={theme}>
{children}
</StyledSystemProvider>
</DarkModeContext.Provider>
);
};
tip

Wrap createTheme in useMemo when the theme depends on dynamic values (e.g. dark mode, safe area insets). This avoids creating a new object every render.

Components reference semantic tokens (bg={'background'}, color={'foreground'}) and automatically adapt to the active mode.

System appearance

Use useColorScheme to sync with the device theme:

import { useColorScheme } from 'react-native';

const colorScheme = useColorScheme();
const isDarkMode = colorScheme === 'dark';

Building a theme from scratch

import { createTheme } from '@react-native-styled-system/util';

const myTheme = createTheme({
space: { '0': 0, '1': 4, '2': 8, '3': 12, '4': 16 },
sizes: { '0': 0, '1': 4, '2': 8, full: '100%' },
colors: { primary: '#3B82F6', background: '#FFFFFF', text: '#111827' },
radii: { sm: 4, md: 8, lg: 16 },
typography: {
h1: { fontSize: 32, fontWeight: 'bold', lineHeight: 40 },
body: { fontSize: 16, lineHeight: 24 },
},
breakpoints: [480, 768, 1024],
});
tip

Use defaultTheme as a starting point and override only what you need. Building from scratch means you lose the Tailwind palette and default spacing scale.