Unistyles 3 is a styling library for React Native that keeps the StyleSheet API and adds themes, breakpoints and variants. Its engine is written in C++ and updates the native views bound to each style, so a theme change or an orientation change does not re-render your components. In an Expo app that speed has one price: Unistyles includes custom native code, so Expo Go cannot run it, and you need a development build.
This guide wires Unistyles 3 into an Expo Router app the way ReadyNative's Unistyles module does. It covers the files setup writes, the import order that breaks most first attempts, themes and breakpoints, switching light and dark, and the gotchas that cost an afternoon.
Who should pick Unistyles
Pick Unistyles when your app has two or more themes, breakpoints that reshape layouts, and a team that already builds a development app. The docs are direct about the limits: Unistyles has no components of its own, so you build the design system yourself, and they point Tailwind-first web projects at uniwind instead.
Setup: one command, then a development build
Run the setup from the repo root. It adds react-native-unistyles ^3.3.0 and react-native-nitro-modules 0.37.1, and it writes the files described below.
bun run setup --ui unistylesThen:
- Make a development build with npx expo run:ios or npx expo run:android, or with bunx eas-cli build --profile development.
- Set your colours in src/theme/tokens.ts. src/unistyles.ts builds both themes from them, with no generate step.
- Run bun run start -- -c once, because the Babel plugin and the entry file both changed.
Setup pins react-native-nitro-modules to 0.37.1 exactly. The Unistyles docs ask for a fixed Nitro version, so keep it there when you upgrade. Unistyles 3 needs the New Architecture, React Native 0.85 or newer, and Expo SDK 56 or newer. The template is on Expo SDK 57 and React Native 0.86, so both minimums are met.
After you add a native library or change the app config, rebuild with npx expo prebuild --clean and run the build command again. Expo's development build guide gives the same rule.
What setup writes, and why the import order matters
Setup writes a few files. Two of them decide whether the app starts cleanly: the Babel config and the entry file. The Babel plugin does its work at build time, and its root option limits it to your sources under src:
["react-native-unistyles/plugin", { root: "src" }]The order matters more. Unistyles parses a StyleSheet as soon as the file that holds it is imported, and StyleSheet.configure must run before the first StyleSheet.create. So setup points package.json main at index.ts, and index.ts imports the config before Expo Router's entry:
import "./src/unistyles";
import "expo-router/entry";src/unistyles.ts registers the themes and breakpoints. src/app/+html.tsx imports the same file for the web build, and once setup has finished, the primitives sit in src/components/ui/.
Themes and breakpoints live in one file
src/unistyles.ts builds the light and dark themes from the design tokens, registers the breakpoints and turns on adaptive themes. Adaptive themes are why the two themes have those names: Unistyles follows the OS colour scheme and picks between them.
import { StyleSheet } from "react-native-unistyles";
const light = {
colors: colors.light,
space,
radius,
fontSize,
lineHeight,
fontWeight,
fontFamily,
} as const;
const dark = { ...light, colors: colors.dark } as const;
export const appThemes = { light, dark } as const;
export const breakpoints = { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200 } as const;
// The same file declares the theme and breakpoint types, for autocomplete.
StyleSheet.configure({
themes: appThemes,
breakpoints,
settings: { adaptiveThemes: true },
});The first breakpoint must be 0, because everything below sm (576) resolves to xs, the way CSS media queries cascade. Don't set initialTheme next to adaptiveThemes. Unistyles throws when both are set.
To change one property at a breakpoint, give it an object keyed by breakpoint name:
const styles = StyleSheet.create((theme) => ({
screen: {
paddingHorizontal: { xs: theme.space[4], md: theme.space[8] },
},
}));Styles in your own components
Import StyleSheet from react-native-unistyles and write a theme function. There is no provider to wrap. The Babel plugin binds the styles to the view, and the component keeps its JSX.
import { StyleSheet } from "react-native-unistyles";
const styles = StyleSheet.create((theme) => ({
card: { backgroundColor: theme.colors.card, padding: theme.space[4] },
}));For looks that depend on a prop, declare variants and pick one with useVariants. Here the tone prop chooses the colour of a card:
import { View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
const styles = StyleSheet.create((theme) => ({
card: {
padding: theme.space[4],
variants: {
tone: {
neutral: { backgroundColor: theme.colors.card },
danger: { backgroundColor: theme.colors.destructive },
},
},
},
}));
export function Notice({ tone = "neutral" }: { tone?: "neutral" | "danger" }) {
styles.useVariants({ tone });
return <View style={styles.card} />;
}Switching between light and dark
A theme change does not re-render your components. The C++ side finds the styles that read the theme, and it updates only those views in the shadow tree:
Use setThemeMode from @/components/ui with system, light or dark. ThemeProvider reads that preference and pushes it into Unistyles. System turns adaptive themes on. An explicit mode turns them off and selects that theme.
import { setThemeMode } from "@/components/ui";
setThemeMode("dark");If you call the runtime yourself, mind the order. setTheme throws while adaptive themes are on, so turn them off first:
import { UnistylesRuntime } from "react-native-unistyles";
UnistylesRuntime.setAdaptiveThemes(false);
UnistylesRuntime.setTheme("dark");To preview dark mode without touching the device, set the colour scheme from react-native. Adaptive themes follow that call, and the unspecified value hands control back to the device settings:
import { Appearance } from "react-native";
Appearance.setColorScheme("dark");
Appearance.setColorScheme("unspecified"); // back to the device settingTests see base styles only
Add react-native-unistyles/mocks to the Jest setupFiles, before your Unistyles config. The official mocks then stand in for the native runtime, and they have limits that matter for a test suite. Variants are stripped, so useVariants does nothing. Styles resolve against the first registered theme, which is light, so adaptive themes are ignored. Screen size and insets read as zero.
Keep Jest for behaviour and text, where the jest-expo setup post starts. Check colours and layout on a device with the Maestro flows the template ships, run with bun run test:e2e against a development build.
Gotchas
- No Expo Go. bun run doctor flags it in its devBuild check. npx expo export still works, because it builds only the JavaScript bundle.
- Spread styles lose their native state. Spreading two Unistyles styles into one object drops the C++ state, and Unistyles warns in development. Pass an array instead.
- Web is best-effort. ReadyNative builds for native first and treats the web export as best-effort, so test a web build before you rely on one.
// Warns in development, and the merge order is unpredictable
<View style={{ ...styles.card, ...styles.highlight }} />
// Use the array form
<View style={[styles.card, styles.highlight]} />Read next: Expo Go vs development build, for what the dev build replaces, then the Unistyles module docs.
FAQ
What is Unistyles 3?
A styling library for React Native with the StyleSheet API, plus themes, breakpoints and variants. Its engine is written in C++ and updates styles in the native shadow tree, so a theme change does not re-render components.
Does Unistyles work in Expo Go?
No. Unistyles includes custom native code, which Expo Go does not run. Build with npx expo run:ios, npx expo run:android or bunx eas-cli build --profile development, and open the app from that build.
Why does setTheme throw in Unistyles?
setTheme throws while adaptive themes are on. Call UnistylesRuntime.setAdaptiveThemes(false) first, or use setThemeMode from @/components/ui, which turns adaptive themes off for light or dark.
Does Unistyles work with Expo Router?
Yes, with an entry file. Point package.json main at index.ts and import the Unistyles config before expo-router/entry, so every StyleSheet is created after configure runs. ReadyNative's setup writes that file.
More guides
- Expo environment variables: which are public?
- TanStack Query in Expo: offline and refetch
- Expo zustand: persist a store across restarts
- jest-expo setup, Expo Router tests and fixes
- Expo auth: Clerk vs Supabase vs Better Auth
- AsyncStorage vs MMKV vs expo-sqlite/kv-store
- Expo i18n: i18next or Lingui, compared
- Expo android widgets: expo-widgets on SDK 58
- Tamagui in Expo: setup, styling and themes
- Best paywall tools for Expo in 2026
- Expo widgets on iOS: a working setup
- RevenueCat subscriptions in an Expo Router app
- Supabase auth in Expo: email, Apple and Google
- Expo push notifications: setup, token, testing
- Expo Go vs development build: limitations
- The best Expo boilerplates, compared