Skip to content

UI and Theming

How we manage the UI and theming of the application.

Why Tailwind CSS ?

For the past few years, we have tried multiple approaches to style our React Native apps: Stylesheet API, styled-components, restyle, and more.

Right now, we are confident that using Tailwind CSS with React Native is the right solution, especially after trying Nativewind.

If you are familiar with Tailwind CSS on the web you will find it very easy to use and you can even copy past your styling from a web application and should work without issues with react native too with some minor adjustments of course.

Last but not least, Tailwind CSS was a natural choice for us, considering that most of our team members come from a web background and have had the opportunity to work with Tailwind CSS before.

About Nativewind

Nativewind is a library that allows you to use Tailwind CSS with react native. Nativewind achieves this by pre-compiling the Tailwind CSS classes into react native stylesheets with a minimal runtime to selectively apply the styles.

NativeWind version 4 introduces several improvements and enhancements and provides a more efficient development experience. The transition to version 4 introduces a different approach that eliminates the need for creating and wrapping our own components with the styled component. Thereby, this utility-first approach simplifies the styling process by using classes and applying styles directly within JSX elements.

For more details about Nativewind you can check their documentation.

Here is an example of how your component should look like:

src/components/card.tsx
import { Link } from 'expo-router';
import type { Post } from '@/api';
import { Image, Pressable, Text, View } from '@/ui';
type Props = Post;
const images = [
'https://images.unsplash.com/photo-1489749798305-4fea3ae63d43?auto=format&fit=crop&w=800&q=80',
'https://images.unsplash.com/photo-1564507004663-b6dfb3c824d5?auto=format&fit=crop&w=800&q=80',
'https://images.unsplash.com/photo-1515386474292-47555758ef2e?auto=format&fit=crop&w=800&q=80',
'https://plus.unsplash.com/premium_photo-1666815503002-5f07a44ac8fb?auto=format&fit=crop&w=800&q=80',
'https://images.unsplash.com/photo-1587974928442-77dc3e0dba72?auto=format&fit=crop&w=800&q=80',
];
export const Card = ({ title, body, id }: Props) => (
<Link href={`/feed/${id}`} asChild>
<Pressable>
<View className="m-2 overflow-hidden rounded-xl border border-neutral-300 bg-white dark:bg-neutral-900">
<Image
className="h-56 w-full overflow-hidden rounded-t-xl"
contentFit="cover"
source={{
uri: images[Math.floor(Math.random() * images.length)],
}}
/>
<View className="p-2">
<Text className="py-3 text-2xl ">{title}</Text>
<Text numberOfLines={3} className="leading-snug text-gray-600">
{body}
</Text>
</View>
</View>
</Pressable>
</Link>
);

Configuration

Nativewind is the same as Tailwind CSS, it comes with a default theme and colors that you can override by creating your own theme and colors.

You need to understand that Nativewind is a library that is built on top of Tailwind CSS. Feel free to add any Tailwind CSS config that you want to use in your application such as updating colors, spacing, typography, etc.

We have created a ui/theme folder where you can find our custom colors that have been imported into tailwind.config.js and used as a theme for our demo application. You can add your own color palette and use them in your components with Tailwind class names.

You can read more about how to configure your project with Tailwind CSS.

Dark Mode

Why dark mode?

Dark mode has gained significant traction in recent years and has become an expected feature to have. By applying dark mode, it makes it easier on the eyes in low-light environments and reduces eye strain, which means more time spent on your app.

This template comes with dark mode support out of the box, and it’s very easy to customize the color scheme of your app. Thanks to tailwindcss

Implementation

Since we’re using nativewind (which uses Tailwind CSS under the hood) and expo-router we let them handle the application of theme, and we just take care of the colors we want. We set the colors in ui/theme/colors.js and we use them in our hook useThemeConfig.tsx to get the theme object that we pass to ThemeProvider directly. For more information check out expo-router

src/core/use-theme-config.tsx
import type { Theme } from '@react-navigation/native';
import {
DarkTheme as _DarkTheme,
DefaultTheme,
} from '@react-navigation/native';
import { useColorScheme } from 'nativewind';
import colors from '@/ui/colors';
export const DarkTheme: Theme = {
..._DarkTheme,
colors: {
..._DarkTheme.colors,
primary: colors.primary[200],
background: colors.charcoal[950],
text: colors.charcoal[100],
border: colors.charcoal[500],
card: colors.charcoal[850],
},
};
export const LightTheme: Theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: colors.primary[400],
background: colors.white,
},
};
export function useThemeConfig() {
const { colorScheme } = useColorScheme();
if (colorScheme === 'dark') {
return DarkTheme;
}
return LightTheme;
}
src/app/_layout.tsx
// Import global CSS file
import '../../global.css';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { ThemeProvider } from '@react-navigation/native';
import { SplashScreen, Stack } from 'expo-router';
import React from 'react';
import { StyleSheet } from 'react-native';
import FlashMessage from 'react-native-flash-message';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { KeyboardProvider } from 'react-native-keyboard-controller';
import { APIProvider } from '@/api';
import interceptors from '@/api/common/interceptors';
import { hydrateAuth, loadSelectedTheme } from '@/core';
import { useThemeConfig } from '@/core/use-theme-config';
export { ErrorBoundary } from 'expo-router';
export const unstable_settings = {
initialRouteName: '(app)',
};
hydrateAuth();
loadSelectedTheme();
interceptors();
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
return (
<Providers>
<Stack>
<Stack.Screen name="(app)" options={{ headerShown: false }} />
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} />
</Stack>
</Providers>
);
}
function Providers({ children }: { children: React.ReactNode }) {
const theme = useThemeConfig();
return (
<GestureHandlerRootView
style={styles.container}
className={theme.dark ? `dark` : undefined}
>
<KeyboardProvider>
<ThemeProvider value={theme}>
<APIProvider>
<BottomSheetModalProvider>
{children}
<FlashMessage position="top" />
</BottomSheetModalProvider>
</APIProvider>
</ThemeProvider>
</KeyboardProvider>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

How do we handle theme changes?

We use the loadSelectedTheme function to load the theme from the storage if there’s a theme saved in the storage, otherwise, we let nativwind use the default theme (system). To set the selected theme, we use the useSelectedTheme hook, which sets the theme in the storage and updates the color scheme of the app.

src/core/hooks/use-selected-theme.tsx
import { colorScheme, useColorScheme } from 'nativewind';
import { useCallback } from 'react';
import { useMMKVString } from 'react-native-mmkv';
import { storage } from '../storage';
const SELECTED_THEME = 'SELECTED_THEME';
export type ColorSchemeType = 'light' | 'dark' | 'system';
/**
* this hooks should only be used while selecting the theme
* This hooks will return the selected theme which is stored in MMKV
* selectedTheme should be one of the following values 'light', 'dark' or 'system'
* don't use this hooks if you want to use it to style your component based on the theme use useColorScheme from nativewind instead
*
*/
export const useSelectedTheme = () => {
const { setColorScheme } = useColorScheme();
const [theme, _setTheme] = useMMKVString(SELECTED_THEME, storage);
const setSelectedTheme = useCallback(
(t: ColorSchemeType) => {
setColorScheme(t);
_setTheme(t);
},
[setColorScheme, _setTheme]
);
const selectedTheme = (theme ?? 'system') as ColorSchemeType;
return { selectedTheme, setSelectedTheme } as const;
};
// to be used in the root file to load the selected theme from MMKV
export const loadSelectedTheme = () => {
const theme = storage.getString(SELECTED_THEME);
if (theme !== undefined) {
colorScheme.set(theme as ColorSchemeType);
}
};

Add dark mode for each component

To add the values for the light mode, you can simply write them directly in your component class. For the dark mode, you can use the dark: variant.

<View className="... border-neutral-200 dark:border-yellow-700">....</View>

If you want to use the style prop, you can use the useColorScheme hook to get the current color scheme and use it to apply the desired style. However, in most cases, you won’t need it as the dark: variant will do the job.

import { useColorScheme } from 'nativewind';
const colorScheme = useColorScheme();
const style =
colorScheme === 'dark'
? { backgroundColor: 'black' }
: { backgroundColor: 'white' };

For more details about dark mode, you can check tailwind and nativewind