Building high-quality UIs with Expo and NativeWind

DevelopmentReact Native3 minutes read

Thomino

Thomino

Guest Author

Learn how to design React Native apps directly in code using Expo, NativeWind, and Reanimated. Build reusable components, implement theming, and skip Figma.

How to build high-quality UIs with Expo and NativeWind

This is a guest post from Thomino - he is the creator of Native Templates and is a great follow on X.

I have been building for the web since the era of IE6. I remember using sliced images for box shadows, transparent GIFs for rounded corners, and nested tables for structure. Those were the "Dark Ages" of front-end development.

Luckily, times have changed. With the combination of Expo, NativeWind and Reanimated designing apps is a breeze. In fact, I don’t even use Figma anymore. I design directly in code with Expo Go live preview.

Let me show you how I build my apps with the reusable components that I use in my workflows. These components allow me to design directly in the editor. I’ll also cover how I handle theming and how to build an animated Theme toggle.

Reusable Components

Each app is different, but most of them share many screens, flows and components. That’s why I decided to build templates that I can always use as a base on a new project according to my needs. The key to doing this effectively was to create reusable components that could be styled easily according to the particular app.

Here are a few examples I’ve implemented in NativeTemplates.

Building a flexible header component with NativeWind

Arguably the most important and fundamental one. It was very important to make the header component flexible and easy to adjust according to one’s needs.

Code
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { useThemeColors } from "app/contexts/ThemeColors";
import { Link, router } from "expo-router";
import Icon, { IconName } from "./Icon";
import { useSafeAreaInsets } from "react-native-safe-area-context";
type HeaderProps = {
title: string,
showBackButton?: boolean,
rightComponents?: React.ReactNode[],
};
const Header: React.FC<HeaderProps> = ({
title,
showBackButton = false,
rightComponents = [],
}) => {
const colors = useThemeColors();
const insets = useSafeAreaInsets();
const handleBackPress = () => {
router.back();
};
return (
<View
style={{ paddingTop: insets.top }}
className="w-full pb-2 flex-row justify-between px-6 bg-background"
>
<View className="flex-row items-center flex-1">
{showBackButton && (
<TouchableOpacity onPress={handleBackPress} className="mr-4 py-4">
<Icon name="ArrowLeft" size={24} color={colors.icon} />
</TouchableOpacity>
)}
<View className="py-4">
<Text className="text-lg font-bold" style={{ color: colors.text }}>
{title}
</Text>
</View>
</View>
{rightComponents.length > 0 && (
<View className="flex-row items-center justify-end flex-1">
{rightComponents.map((component, index) => (
<View key={index} className="ml-6">
{component}
</View>
))}
</View>
)}
</View>
);
};
export default Header;
Code
// Basic header with title
<Header title="Home" />
// Header with back button
<Header
title="Details"
showBackButton
/>
// Header with action icons
<Header
title="Messages"
rightComponents={[
<HeaderIcon key="search" icon="Search" href="/search" />,
<HeaderIcon key="settings" icon="Settings" href="/settings" />
]}
/>

Creating animated tab navigation in React Native

Choose an icon, animation or avatar.

Code
<TabTrigger name="home" href="/" asChild>
<TabButton labelAnimated={true} icon="Home">Home</TabButton>
</TabTrigger>
<TabTrigger name="profile" href="/profile" asChild>
<TabButton labelAnimated={true} avatar={require('@/assets/img/thomino.jpg')}>Profile</TabButton>
</TabTrigger>

Building multi-step forms and onboarding flows

I use this one very heavily. It is perfect for on-boardings and other user flows. Onboarding is essential for activating your users and reducing that early churn.

Code
<MultiStep
onComplete={handleComplete}
onClose={handleClose}
headerTitle="Get Started"
>
<Step title="Profile">
<Profile />
</Step>
<Step title="Role">
<Role />
</Step>
<Step title="Capabilities">
<Capabilities />
</Step>
<Step title="Review">
<Review />
</Step>
</MultiStep>;

Reusable chip components for filters and forms

This one is very basic but used heavily for filters or forms.

Code
<Chip
label="Custom"
className="my-2 mx-1"
size="xl"
/>

You can check all the components in my docs.

Implementing theme variables with NativeWind

NativeWind makes theming incredibly powerful. By using vars, we can toggle the entire theme of an app in one file. This is how I ensure that light and dark modes aren't just an afterthought, but a core part of the design system.

Code
import { vars } from "nativewind";
export const themes = {
light: vars({
"--color-primary": "#000000",
"--color-invert": "#ffffff",
"--color-secondary": "#ffffff",
"--color-background": "#F4F4F5",
"--color-darker": "#F4F4F5",
"--color-text": "#000000",
"--color-highlight": "#7E55D8",
"--color-border": "rgba(0, 0, 0, 0.15)",
}),
dark: vars({
"--color-primary": "#ffffff",
"--color-invert": "#000000",
"--color-secondary": "#1e1e1e",
"--color-background": "#141414",
"--color-darker": "#000000",
"--color-text": "#ffffff",
"--color-highlight": "#7E55D8",
"--color-border": "rgba(255, 255, 255, 0.15)",
}),
};

Building an animated dark mode toggle with Reanimated

Code
import { Pressable, View } from "react-native";
import { useTheme } from "@/app/contexts/ThemeContext";
import Feather from "@expo/vector-icons/Feather";
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from "react-native-reanimated";
import { useEffect } from "react";
const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme();
const isDark = theme === "dark";
const translateX = useSharedValue(isDark ? 36 : 3.5);
useEffect(() => {
translateX.value = withSpring(isDark ? 36 : 3.5, {
damping: 15,
stiffness: 150,
});
}, [isDark]);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }],
};
});
return (
<Pressable
onPress={toggleTheme}
className="w-20 h-10 p-1 bg-secondary relative flex-row rounded-full items-center justify-between"
>
<Icon icon="sun" />
<Icon icon="moon" />
<Animated.View
style={[animatedStyle]}
className="w-9 h-9 bg-background rounded-full items-center justify-center flex flex-row absolute"
/>
</Pressable>
);
};
const Icon = (props: any) => {
const { theme } = useTheme();
const isDark = theme === "dark";
return (
<View className="w-9 h-9 relative z-50 rounded-full items-center justify-center flex flex-row">
<Feather
name={props.icon}
size={16}
color={`${isDark ? "white" : "black"}`}
/>
</View>
);
};
export default ThemeToggle;

Pre-built screen templates for Login, Onboarding, and Settings

With a solid component base, building templates is super fast. There are screens that almost every app has in common with like Login, Signup, Onboarding, Profile Settings, Privacy, etc. But here, all the components come to life!

Code
export default function ProfileScreen() {
return (
<View className="flex-1 bg-light-primary dark:bg-dark-primary">
<Header
leftComponent={<ThemeToggle />}
rightComponents={[
<HeaderIcon icon="ChartBar" href="/screens/analytics" />,
]}
/>
<View className="flex-1 bg-light-primary dark:bg-dark-primary">
<ThemedScroller>
<AnimatedView className="pt-4" animation="scaleIn">
<SubscriptionCard />
<View className="gap-1 bg-secondary rounded-3xl">
<ListLink
className="px-4 py-2 border-b border-border"
showChevron
title="Account settings"
icon="Settings"
href="/screens/settings"
/>
<ListLink
className="px-4 py-2 border-b border-border"
showChevron
title="Billing"
icon="CreditCard"
href="/screens/billing"
/>
</View>
</AnimatedView>
</ThemedScroller>
</View>
</View>
);
}

Expo has given us the platform, and NativeWind has given us the styling language to make apps that don't just work but also look amazing.

If you're looking to jumpstart your next project, you can explore all React Native Expo Templates here or my Expo Playground on GitHub.

NativeWind
React Native

Dive in, and create your first Expo project

Learn more