AccueilClientsExpertisesOpen SourceBlogContactEstimer

16 septembre 2025

Style Your React Native App with Unistyles

7 minutes reading

Style Your React Native App with Unistyles
🇫🇷 This post is also available in french

While the new React Native architecture has reached a stable state, the pursuit of performance is a never-ending story. Choosing the right styling library for your app is not always straightforward. It needs to combine performance, flexibility, and ease of use. In a previous article, we explored Tamagui, which is simple to use but can be cumbersome to configure if you want a highly customized design.

More recently, NativeWind has gained a lot of traction, leveraging the widespread adoption of Tailwind in web development. NativeWind is simple to set up and has an API almost identical to Tailwind’s, making it easy for web developers to get started with React Native projects. However, its internal workings can be tricky to understand, and you sometimes end up being verbose when building very generic components (for example, using remapProps or cssInterop).

Within this race for better performance and developer experience, Unistyles has released its version 3. In this article, we’ll explore what this library brings to the table and its main strengths.

How Does It Work?

Unistyles’ mission is to provide an API very close to the React Native StyleSheet, while offering additional features like theming, breakpoint support, and variants. Pushing further, Unistyles boosts performance by handling styles directly within the application’s C++ layer. Essentially, this means you can associate a style directly with a native node in your component tree, and that node will react to style changes (like theme switches or viewport-dependent props) without triggering a re-render of your JavaScript component tree. All these optimizations are powered by a Babel plugin.

Getting Started

Let’s get hands-on with Unistyles by creating an Expo project. We'll use the "Blank" template:

bun create expo-app demo-unistyles -t
cd demo-unistyles
npx expo install react-native-unistyles react-native-nitro-modules react-native-reanimated react-native-edge-to-edge

Add the Babel plugin:

npx expo customize babel.config.js
babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      [
        "react-native-unistyles/plugin",
        {
          root: "src", // Root directory containing styled components
        },
      ],
    ],
  };
};

Now, let's define a simple style in the App component:

src/app.tsx
import { StyleSheet } from 'react-native-unistyles';
import { View } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#FFFFFF',
  },
});

const App = () => {
  return <View style={styles.container} />
}

export default App;

As you can see, Unistyles' API, as-is, has a 1:1 parity with React Native’s StyleSheet API, making migration to Unistyles a breeze.

Theming

Let’s explore a key Unistyles feature over plain StyleSheet: theming.

Create a src/themes.ts file:

src/themes.ts
import { StyleSheet } from "react-native-unistyles";

// Some utility functions
const spacing = (size: number) => size * 4;
const radius = (size: number) => size * 4;

const utils = {
  spacing,
  radius,
};

export const light = {
  colors: {
    primary: "#007AFF",
    red: "#ed3e3e",
    background: "#ffffff",
  },
  utils,
};

export const dark = {
  colors: {
    primary: "#4da6ff",
    red: "#fa645c",
    background: "#3E3E3E",
  },
  utils,
};

StyleSheet.configure({
  themes: {
    light,
    dark,
  },
  settings: {
    /**
     * Naming the themes "dark" and "light" allows you to sync them automatically
     * with the operating system's appearance. To enable this, make sure your
     * Expo config sets `userInterfaceStyle` to "automatic".
     */
    adaptiveThemes: true,
  },
});

type AppThemes = {
  light: typeof light;
  dark: typeof dark;
};

declare module "react-native-unistyles" {
  export interface UnistylesThemes extends AppThemes {}
}

Here, we set up two color themes, each designed to adapt to the OS-level theme. Of course, you can add others with different names if you want. For themes to be initialized before the first render, make sure to import this file from index.ts:

index.ts
import { registerRootComponent } from "expo";
import "./src/theme";
import App from "./src/app";

registerRootComponent(App);

Let’s use these themes! You’ll need to slightly change your StyleSheet.create function. It now accepts a function that gets two parameters: the active theme, and some device-specific styling variables (we’ll talk about these shortly).

src/app.tsx
import { StyleSheet } from 'react-native-unistyles';
import { View } from 'react-native';

const styles = StyleSheet.create((theme, rt) => ({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: theme.colors.background,
    padding: theme.utils.spacing(4)
  },
}));

const App = () => {
  return <View style={styles.container} />
}

export default App;

Now, if you switch between light and dark mode on your device, the background color updates automatically—with no React tree re-render!

Responsive Styles

You can define styles based on device size or orientation. By default, Unistyles lets you provide styles for orientation-based breakpoints, but you're free to declare your own breakpoints based on screen size. For example, let’s change the background color depending on orientation:

src/app.tsx
import { StyleSheet } from 'react-native-unistyles';
import { View } from 'react-native';

const styles = StyleSheet.create((theme, rt) => ({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4)
  },
}));

const App = () => {
  return <View style={styles.container} />
}

export default App;

Change your device's orientation, and the background color adapts—again, with no unnecessary re-render!

Unistyles Runtime

Now, let’s discuss runtime variables, as mentioned earlier. Unistyles exposes runtime values—device-specific properties accessible at any time. There are two runtime concepts:

  • The global runtime, which exposes info and functions like getting/setting the theme, reading the theme contents, changing your native view’s background color, and more.
  • The mini runtime, which exposes similar variables but is designed to be used inside your style functions. This way, your styles remain reactive to changes in these device variables.

Let’s use the mini runtime to add top spacing based on the safe area insets:

src/app.tsx
import { StyleSheet } from 'react-native-unistyles';
import { View } from 'react-native';

const styles = StyleSheet.create((theme, rt) => ({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
  },
}));

const App = () => {
  return <View style={styles.container} />
}

export default App;

Variants

Much like Tamagui and other style libraries, Unistyles allows you to define variants for specific styles. A classic use case: building buttons. To make things a bit more interesting, we’ll animate our button color whenever it changes.

src/button.tsx
import { PropsWithChildren } from "react";
import {
  Pressable,
  type PressableProps,
  StyleProp,
  Text,
  ViewStyle,
} from "react-native";
import Animated, {
  useAnimatedStyle,
  withTiming,
} from "react-native-reanimated";
import { StyleSheet, type UnistylesVariants } from "react-native-unistyles";
import { useAnimatedVariantColor } from "react-native-unistyles/reanimated";

type Props = UnistylesVariants<typeof styles> &
  Omit<PressableProps, "style"> & {
    label: string;
    style?: StyleProp<ViewStyle>;
  };

const Button = ({
  type = "primary",
  style,
  label,
  children,
  ...props
}: Props) => {
  // Enable the variant for the Button component
  styles.useVariants({ type });
  // Get the background color as a shared value
  const color = useAnimatedVariantColor(styles.container, "backgroundColor");
  // Animate the background color
  const animatedStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: withTiming(color.value, { duration: 500 }),
    };
  });

  return (
    <Pressable {...props}>
      <Animated.View style={[styles.container, animatedStyle, style]}>
        <ButtonText type={type}>{label}</ButtonText>
      </Animated.View>
    </Pressable>
  );
};

const ButtonText = ({
  children,
  type,
}: PropsWithChildren<UnistylesVariants<typeof styles>>) => {
  // Enable the variant for the ButtonText component
  styles.useVariants({ type });

  return <Text style={styles.text}>{children}</Text>;
};

export default Button;

const styles = StyleSheet.create((theme, rt) => ({
  container: {
    borderRadius: theme.utils.radius(2),
    flexDirection: "row",
    paddingHorizontal: theme.utils.spacing(4),
    paddingVertical: theme.utils.spacing(3),

    variants: {
      type: {
        primary: {
          backgroundColor: theme.colors.primary,
        },
        destructive: {
          backgroundColor: theme.colors.red,
        },
      },
    },
  },
  text: {
    fontSize: 14,
    variants: {
      type: {
        primary: {
          color: "white",
        },
        destructive: {
          color: "white",
        },
      },
    },
  },
}));

Here, we've defined two variants for our buttons. You can now set these variants on the Button component via its props.

src/app.tsx
import { StyleSheet } from 'react-native-unistyles';
import { View } from 'react-native';
import Button from './button';

const styles = StyleSheet.create((theme, rt) => ({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
    gap: theme.utils.spacing(4),
  },
}));

const App = () => {
  return (
    <View style={styles.container}>
      <Button type="primary" label="Primary Button" />
      <Button type="destructive" label="Destructive Button" />
    </View>
  )
}

export default App;

Let’s also make use of the global runtime. We’ll set up our buttons to switch the active color theme.

src/app.tsx
import { StyleSheet, UnistylesRuntime } from 'react-native-unistyles';
import { View } from 'react-native';
import Button from './button';

const styles = StyleSheet.create((theme, rt) => ({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
    gap: theme.utils.spacing(4),
  },
}));

const App = () => {
  return (
    <View style={styles.container}>
      <Button type="primary" label="Light mode" onPress={() => UnistylesRuntime.setTheme('light')} />
      <Button type="destructive" label="Dark mode" onPress={() => UnistylesRuntime.setTheme('dark')} />
    </View>
  )
}

export default App;

Now, whenever you press one of the buttons, the background color updates right away—still with no unnecessary re-render.

Beyond basic variants, Unistyles lets you define variant combinations called compound variants, for even more styling flexibility.

Conclusion

Unistyles is a compelling alternative to existing solutions thanks to its performance-oriented design, ease of use, and close alignment with the StyleSheet API. Bonus: Web support is also available. So, Unistyles or Tamagui? The answer depends on your preferences, but with its ease of configuration, Unistyles strikes me as an excellent choice. Still, if you're a die-hard Tailwind fan, you should know the Unistyles creator is working on Uniwind (not yet released at the time of writing), aiming to combine Unistyles’ performance with Tailwind’s strengths.

Got a React Native project in mind, or thinking about migrating your app to Unistyles? Contact us—we’d love to help!

À découvrir également

Premier Octet vous accompagne dans le développement de vos projets avec react

En savoir plusNous contacter
18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub

X

Flux RSS

Bluesky

Naviguez à vue