How to build type-enforced UI components in React Native using @shopify/restyle
by Ilias Haddad
It’s been quite some time since I wrote a technical post on my blog, and here’s a new one about building type-enforced UI components in React Native with @shopify/restyle and expo.
@shopify/restyle
is a powerful styling library for React Native that brings type safety and consistency to your UI components. Unlike traditional styling approaches, Restyle allows you to create a centralized theme configuration that enforces design system principles across your entire application.
Getting Started
Project Setup
- Setup your react native project using expo
npx create-expo-app@latest
- Go to your project directory and install @shopify/restyle package using expo
cd /path/to/projectnpx expo install @shopify/restyle
Creating Your Theme
Create a theme.tsx
file to define your design system:
touch theme.tsx
- Copy and paste the default theme configuration
import {createTheme} from '@shopify/restyle';const palette = {purpleLight: '#8C6FF7',purplePrimary: '#5A31F4',purpleDark: '#3F22AB',greenLight: '#56DCBA',greenPrimary: '#0ECD9D',greenDark: '#0A906E',black: '#0B0B0B',white: '#F0F2F3',};const theme = createTheme({colors: {mainBackground: palette.white,cardPrimaryBackground: palette.purplePrimary,},spacing: {s: 8,m: 16,l: 24,xl: 40,},textVariants: {header: {fontWeight: 'bold',fontSize: 34,},body: {fontSize: 16,lineHeight: 24,},defaults: {// We can define a default text variant here.},},});export type Theme = typeof theme;export default theme;
Implementing Theme Provider
Update your app/_layout.tsx
:
import { DarkTheme, DefaultTheme } from "@react-navigation/native";import { useFonts } from "expo-font";import { Stack } from "expo-router";import * as SplashScreen from "expo-splash-screen";import { StatusBar } from "expo-status-bar";import { useEffect } from "react";import "react-native-reanimated";import { ThemeProvider } from "@shopify/restyle";import theme from "@/theme";// Prevent the splash screen from auto-hiding before asset loading is complete.SplashScreen.preventAutoHideAsync();export default function RootLayout() {const [loaded] = useFonts({SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),});useEffect(() => {if (loaded) {SplashScreen.hideAsync();}}, [loaded]);if (!loaded) {return null;}return (<ThemeProvider theme={theme}><Stack><Stack.Screen name="(tabs)" options={{ headerShown: false }} /><Stack.Screen name="+not-found" /></Stack><StatusBar style="auto" /></ThemeProvider>);}
Creating Reusable Components
Text Component
touch components/Text.tsx
// In components/Text.tsximport {createText} from '@shopify/restyle';import {Theme} from '../theme';export const Text = createText<Theme>();
Let’s use it in our home screen
import { Text } from "@/components/Text";import { SafeAreaView } from "react-native-safe-area-context";export default function HomeScreen() {return (<SafeAreaView><Text margin="m" variant="header">This is the Home screen. Built using @shopify/restyle.</Text></SafeAreaView>);}
As you can see in the code above, we’re passing margin as a “m” instead of a number. We’re getting the value from our theme.tsx
file.
// ./theme.tsxconst theme = createTheme({spacing: {s: 8,m: 16, // margin="m"l: 24,xl: 40,},textVariants: {header: { // our text header variantfontWeight: 'bold',fontSize: 34,},body: {fontSize: 16,lineHeight: 24,},},// ...rest of code},});
This how our the home page view will looks
Skeleton Loader Component
Let’s build a Skeleton Loader
card
touch components/SkeletonLoader.tsx
// components/SkeletonLoader.tsximport {BackgroundColorProps,createBox,createRestyleComponent,createVariant,spacing,SpacingProps,VariantProps,} from "@shopify/restyle";import { Theme } from "@/theme";import { View } from "react-native";const Box = createBox<Theme>();type Props = SpacingProps<Theme> &VariantProps<Theme, "cardVariants"> &BackgroundColorProps<Theme> &React.ComponentProps<typeof View>;const CardSkeleton = createRestyleComponent<Props, Theme>([spacing,createVariant({ themeKey: "cardVariants" }),]);const SkeletonLoader = () => {return (<CardSkeleton variant="elevated"><BoxbackgroundColor="cardPrimaryBackground"height={20}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}></Box><BoxbackgroundColor="cardPrimaryBackground"height={100}marginBottom="s"width="90%"overflow="hidden"borderRadius={"m"}></Box><BoxbackgroundColor="cardPrimaryBackground"height={50}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}></Box></CardSkeleton>);};export default SkeletonLoader;
- We create a new box as a predefined component from @shopify/restyle package and this will how us to create the Skeleton Box
const Box = createBox<Theme>();
- Create a new
CardSkeleton
component using the createStyleComponent to create a custom component and we passed props which are spacing andcardVariants
that we have to define in ourtheme.tsx
file
type Props = SpacingProps<Theme> &VariantProps<Theme, "cardVariants"> &BackgroundColorProps<Theme> &React.ComponentProps<typeof View>;const CardSkeleton = createRestyleComponent<Props, Theme>([spacing,createVariant({ themeKey: "cardVariants" }),]);
- Create a
SkeletonLoader
Component to render our Skelton Card component
// components/SkeletonLoader.tsxexport const SkeletonLoader = () => {return (<CardSkeleton variant="elevated"><BoxbackgroundColor="cardPrimaryBackground"height={20}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}></Box><BoxbackgroundColor="cardPrimaryBackground"height={100}marginBottom="s"width="90%"overflow="hidden"borderRadius={"m"}></Box><BoxbackgroundColor="cardPrimaryBackground"height={50}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}></Box></CardSkeleton>);};
We have one thing left to make it working, update theme.tsx
file to have cardVariants
const theme = createTheme({colors: {// Add Black Color to use it later onblack: palette.black,},// Add Border Radius VariantsborderRadii: {s: 4,m: 10,l: 25,xl: 75,},// Add Card VariantscardVariants: {elevated: {shadowColor: "black",shadowOffset: { width: 0, height: 2 },shadowOpacity: 0.1,shadowRadius: 4,elevation: 3,borderRadius: "m",},defaults: {padding: "m",borderRadius: "m",},},});
That’s great, but let’s animation to our component
// components/SkeletonLoader.tsxconst ShimmerAnimation = () => {const shimmerTranslate = useRef(new Animated.Value(0)).current;useEffect(() => {Animated.loop(Animated.timing(shimmerTranslate, {toValue: 1,duration: 1500,useNativeDriver: true,})).start();}, [shimmerTranslate]);const translateX = shimmerTranslate.interpolate({inputRange: [0, 1],outputRange: [-300, 300],});return (<Animated.Viewstyle={{position: "absolute",top: 0,left: 0,bottom: 0,width: 100,backgroundColor: "rgba(255,255,255,0.2)",transform: [{ translateX }],}}/>);};
and let’s use it in our Skeleton Loader
Component
// components/SkeletonLoader.tsxexport const SkeletonLoader = () => {return (<CardSkeleton variant="elevated"><BoxbackgroundColor="cardPrimaryBackground"height={20}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}><ShimmerAnimation /></Box><BoxbackgroundColor="cardPrimaryBackground"height={100}marginBottom="s"width="90%"overflow="hidden"borderRadius={"m"}><ShimmerAnimation /></Box><BoxbackgroundColor="cardPrimaryBackground"height={50}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}><ShimmerAnimation /></Box></CardSkeleton>);};
And here’s the full component code:
// components/SkeletonLoader.tsximport { useEffect, useRef } from "react";import { Animated } from "react-native";import {BackgroundColorProps,createBox,createRestyleComponent,createVariant,spacing,SpacingProps,VariantProps,} from "@shopify/restyle";import { Theme } from "@/theme";import { View } from "react-native";const Box = createBox<Theme>();const ShimmerAnimation = () => {const shimmerTranslate = useRef(new Animated.Value(0)).current;useEffect(() => {Animated.loop(Animated.timing(shimmerTranslate, {toValue: 1,duration: 1500,useNativeDriver: true,})).start();}, [shimmerTranslate]);const translateX = shimmerTranslate.interpolate({inputRange: [0, 1],outputRange: [-300, 300],});return (<Animated.Viewstyle={{position: "absolute",top: 0,left: 0,bottom: 0,width: 100,backgroundColor: "rgba(255,255,255,0.2)",transform: [{ translateX }],}}/>);};type Props = SpacingProps<Theme> &VariantProps<Theme, "cardVariants"> &BackgroundColorProps<Theme> &React.ComponentProps<typeof View>;const CardSkeleton = createRestyleComponent<Props, Theme>([spacing,createVariant({ themeKey: "cardVariants" }),]);export const SkeletonLoader = () => {return (<CardSkeleton variant="elevated"><BoxbackgroundColor="cardPrimaryBackground"height={20}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}><ShimmerAnimation /></Box><BoxbackgroundColor="cardPrimaryBackground"height={100}marginBottom="s"width="90%"overflow="hidden"borderRadius={"m"}><ShimmerAnimation /></Box><BoxbackgroundColor="cardPrimaryBackground"height={50}marginBottom="s"width="70%"overflow="hidden"borderRadius={"m"}><ShimmerAnimation /></Box></CardSkeleton>);};
Et voila, we made a skeleton loader card using @shopify/restyle using
Support for dark mode
Let’s start with adding dark theme configuration, in your theme.tsx
file
// theme.tsxexport const darkTheme: Theme = {...theme,colors: {...theme.colors,mainBackground: palette.white,cardPrimaryBackground: palette.purpleDark,greenPrimary: palette.purpleLight,},textVariants: {...theme.textVariants,defaults: {...theme.textVariants.header,color: palette.purpleDark,},},
Add our dark theme configuration in our app layout by adding it to our layout.tsx
file
// app/_layout.tsximport { useFonts } from "expo-font";import { Stack } from "expo-router";import * as SplashScreen from "expo-splash-screen";import { StatusBar } from "expo-status-bar";import { useEffect } from "react";import "react-native-reanimated";import { ThemeProvider } from "@shopify/restyle";import theme, { darkTheme } from "@/theme";import { useColorScheme } from "react-native";// Prevent the splash screen from auto-hiding before asset loading is complete.SplashScreen.preventAutoHideAsync();export default function RootLayout() {const [loaded] = useFonts({SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),});const colorSchema = useColorScheme();useEffect(() => {if (loaded) {SplashScreen.hideAsync();}}, [loaded]);if (!loaded) {return null;}return (<ThemeProvider theme={colorSchema === "dark" ? darkTheme : theme}><Stack><Stack.Screen name="(tabs)" options={{ headerShown: false }} /><Stack.Screen name="+not-found" /></Stack><StatusBar style="auto" /></ThemeProvider>);}
- Get color schema using
useColorScheme
hook from react-native
// app/_layout.tsximport { useColorScheme } from "react-native";//... rest of the codeconst colorSchema = useColorScheme();
- Based on the color schema, use the default light theme or in dark mode use the darkTheme config defined in
theme.tsx
file
// app/_layout.tsximport theme, { darkTheme } from "@/theme";//... rest of the code<ThemeProvider theme={colorSchema === "dark" ? darkTheme : theme}><Stack><Stack.Screen name="(tabs)" options={{ headerShown: false }} /><Stack.Screen name="+not-found" /></Stack><StatusBar style="auto" /></ThemeProvider>
Here’s it dark and light mode.
Et voila, we managed to create type-enforced UI component using @shopify/restyle
package
Thank you :)
- React Native