If you have built anything sizeable in React Native, you have probably hit the same wall: the moment you wire up theming, every theme switch sends a re-render shockwave through half your component tree. Context-based styling libraries make this easy to write and painful to scale. Unistyles takes a different route. It looks and feels almost exactly like React Native's built-in StyleSheet, but underneath it runs a shared C++ core that computes styles and updates the native Shadow Tree directly. The result is that themes, orientation changes, and breakpoint shifts apply without React re-rendering anything.
That combination, a drop-in StyleSheet.create API plus genuinely re-render-free updates, is what makes Unistyles worth a serious look. It supports iOS, Android, web (including Next.js SSR), and Expo, adds themes, breakpoints, media queries, variants, and runtime info like safe-area insets, and claims under 0.1 ms of overhead per StyleSheet. This article focuses on the current v3 architecture, which is a complete rewrite built on Nitro Modules and the New Architecture.
What Sets It Apart
The headline feature of Unistyles 3 is its engine. Styles are not just JavaScript objects living in a hook. A Babel plugin analyzes every StyleSheet at compile time, tags each style with its dependencies (static, theme, or runtime), and binds your views to their parsed styles without inserting any wrapper components. Those styles are then reconstructed on the C++ side as native objects that hold the parsed values, dependency metadata, and bindings to the corresponding Shadow Nodes.
From there the engine listens to roughly sixteen event types, things like theme change, orientation, dimensions, font scale, and insets. When one fires, it recomputes only the affected styles and pushes atomic instructions to mutate the native render tree directly. Your React components never re-render because of a style change. A few more things stand out:
- A familiar API. It is
StyleSheet.create, not a new component kit or a className dialect. Existing code barely changes. - A real web parser. On web it emits actual CSS classes (no
react-native-webdependency) and supports pseudo-classes likehover,focus, andactive. - Themes, breakpoints, and variants with single-call theme switching, adaptive system themes, compound variants, and built-in
portrait/landscapebreakpoints. - Runtime awareness through a mini runtime object exposing insets, screen dimensions, orientation, color scheme, pixel ratio, and more.
- TypeScript-first with module augmentation for full autocomplete on your themes and breakpoints.
Getting It Installed
Unistyles 3 requires React Native 0.78.0 or newer with the New Architecture (Fabric) enabled, since the Shadow Tree manipulation depends on it. Older projects should stay on Unistyles 2.0. Install the library alongside react-native-nitro-modules, which powers the C++/JSI core.
# npm
npm install react-native-unistyles react-native-nitro-modules
# yarn
yarn add react-native-unistyles react-native-nitro-modules
Next, add the Babel plugin. It is not optional, it does the compile-time dependency analysis that makes the whole no-re-render model work.
// babel.config.js
module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'], // or @react-native/babel-preset
plugins: [
['react-native-unistyles/plugin', {
root: 'src' // your app's source folder
}]
]
}
}
Finally, rebuild the native side. Because this is a native dependency, you cannot use it in plain Expo Go.
# Expo
npx expo prebuild --clean
# bare React Native (iOS)
cd ios && pod install
Wiring Up Your Styles
Configuring Once at the Entry Point
Unistyles needs to know about your themes and breakpoints before any styled component is imported. The convention is a unistyles.ts file that you import at the very top of your app entry.
import { StyleSheet } from 'react-native-unistyles'
const lightTheme = {
colors: {
primary: '#007AFF',
background: '#FFFFFF',
text: '#11181C'
},
gap: (v: number) => v * 8
}
const darkTheme = {
colors: {
primary: '#0A84FF',
background: '#11181C',
text: '#ECEDEE'
},
gap: (v: number) => v * 8
}
const breakpoints = {
xs: 0, // the first breakpoint MUST be 0
sm: 576,
md: 768,
lg: 992,
xl: 1200
} as const
type AppThemes = {
light: typeof lightTheme
dark: typeof darkTheme
}
type AppBreakpoints = typeof breakpoints
declare module 'react-native-unistyles' {
export interface UnistylesThemes extends AppThemes {}
export interface UnistylesBreakpoints extends AppBreakpoints {}
}
StyleSheet.configure({
themes: { light: lightTheme, dark: darkTheme },
breakpoints,
settings: { initialTheme: 'light' }
})
The module augmentation block is what gives you full autocomplete and type checking on theme.colors.primary and friends throughout the app. Themes are plain JS objects with no imposed shape, so you can stash colors, spacing scales, component tokens, or helper functions wherever it suits you.
Writing Components Like It Is Just StyleSheet
In a component, the import swap is the only visible change. You import StyleSheet from react-native-unistyles instead of react-native, and everything else looks like the styling you already know.
import { View, Text } from 'react-native'
import { StyleSheet } from 'react-native-unistyles'
export const Card = () => (
<View style={styles.container}>
<Text style={styles.title}>Hello from Unistyles</Text>
</View>
)
const styles = StyleSheet.create(theme => ({
container: {
backgroundColor: theme.colors.background,
padding: theme.gap(2),
borderRadius: 12
},
title: {
color: theme.colors.text,
fontSize: 18,
fontWeight: '600'
}
}))
StyleSheet.create accepts three forms: a static object, a function of theme, and a function of (theme, rt) where rt is the mini runtime. The runtime form is how you pull in live device values without subscribing to anything yourself.
const styles = StyleSheet.create((theme, rt) => ({
screen: {
flex: 1,
backgroundColor: theme.colors.background,
paddingTop: rt.insets.top, // safe-area aware
paddingBottom: rt.insets.bottom
}
}))
When the insets change, say the device rotates or the status bar resizes, only the styles that read rt.insets are recomputed and pushed to the native tree. Nothing re-renders.
Switching Themes Without the Cascade
Theme switching is a single imperative call through UnistylesRuntime. There is no provider to wrap, no context to subscribe to.
import { UnistylesRuntime } from 'react-native-unistyles'
// Manual switch
UnistylesRuntime.setTheme('dark')
// Or let the system drive it
UnistylesRuntime.setAdaptiveThemes(true)
With adaptive themes enabled, Unistyles follows the device color scheme automatically. If you want to flip a theme manually while adaptive mode is on, disable adaptive first, then set the theme. Because the update path runs through C++ straight into the Shadow Tree, even a full app-wide theme change stays smooth on a large screen full of styled views.
Going Further
Variants and Compound Variants
Variants let a single style declaration carry multiple visual modes, which is ideal for design-system components. You declare the options inside the style and then pick them at the call site.
const styles = StyleSheet.create(theme => ({
button: {
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 10,
variants: {
size: {
small: { paddingVertical: 8, paddingHorizontal: 14 },
large: { paddingVertical: 16, paddingHorizontal: 28 }
},
color: {
primary: { backgroundColor: theme.colors.primary },
ghost: { backgroundColor: 'transparent' }
}
},
compoundVariants: [
{
size: 'large',
color: 'ghost',
styles: { borderWidth: 2, borderColor: theme.colors.primary }
}
]
}
}))
You select variants with useVariants, then read the resolved style as usual.
export const Button = ({ size = 'small', color = 'primary' }) => {
styles.useVariants({ size, color })
return (
<Pressable style={styles.button}>
{/* ... */}
</Pressable>
)
}
Compound variants only apply when several conditions match at once, which is how you express rules like "a large ghost button gets a border" without duplicating styles.
Responsive Values and Breakpoints
Any style value can become a responsive object keyed by your breakpoints. Unistyles resolves the right value for the current width using CSS-cascade semantics, which is why the first breakpoint must be 0.
const styles = StyleSheet.create(theme => ({
grid: {
flex: 1,
backgroundColor: { xs: theme.colors.background, lg: theme.colors.primary },
flexDirection: { xs: 'column', md: 'row' },
transform: [{ scale: { xs: 1, xl: 1.1 } }]
}
}))
There are built-in portrait and landscape breakpoints for orientation, and an mq helper plus Display and Hide components for showing or hiding content at specific sizes. The current breakpoint is always readable from UnistylesRuntime.breakpoint if you need it in logic. When the device resizes, only the responsive styles recompute and update the native views.
Reaching Third-Party Components
The no-re-render model works because Unistyles owns the binding between your views and their styles. Third-party components that do not consume styles the Unistyles way need an escape hatch. For those, v3 ships withUnistyles, a higher-order component that injects theme- or runtime-derived props, and useUnistyles, a hook that opts back into ordinary re-render-based access when you genuinely need a value inside render logic.
import { withUnistyles, useUnistyles } from 'react-native-unistyles'
import { LinearGradient } from 'expo-linear-gradient'
// Inject derived props into a non-Unistyles component
const ThemedGradient = withUnistyles(LinearGradient, theme => ({
colors: [theme.colors.primary, theme.colors.background]
}))
// Or read the theme directly inside a component (this one will re-render)
const Badge = () => {
const { theme } = useUnistyles()
return <Text style={{ color: theme.colors.primary }}>New</Text>
}
The guidance is to reach for these sparingly. The whole point of Unistyles is that most of your styled views never re-render, so the escape hatches are there for the edges, not the core. Recent 3.x releases have also expanded what the engine can express natively, including dropShadow support and compatibility with Suspense trees.
Wrapping Up
Unistyles occupies a genuinely useful spot in the React Native styling landscape. It does not ask you to learn a className dialect like NativeWind or adopt a full component kit like Tamagui. Instead it keeps the StyleSheet.create mental model you already have and quietly moves the expensive parts, style computation and view updates, into a C++ core that talks straight to the Shadow Tree. The payoff is theme and orientation changes that stay smooth even on busy screens, plus first-class themes, breakpoints, and variants.
The cost is real but reasonable: you need the New Architecture, RN 0.78 or newer, the Nitro Modules dependency, and the Babel plugin, which means a native rebuild and no Expo Go. If your project clears that bar and you have felt the sting of theming re-renders, Unistyles is one of the most compelling styling options available today. It is MIT licensed, actively maintained, and pulling well over a hundred thousand npm downloads a week, so you are in good company.