Using Material Design / Material UI in a React Native App

This post originally appeared as Using Material UI in React Native on blog.logrocket.com

If you’re building a cross-platform mobile app, it’s a good idea to base your app’s UI/UX on Material Design, Google’s own design language, which it uses in all its mobile apps. The most popular mobile apps heavily use Material Design concepts: Whatsapp, Uber, Lyft, Google Maps, SpotAngels, etc. This means your users are already familiar with the look and feel of Material Design, and they will understand how to use your app more easily if you adhere to the design language of their favorite, most commonly used apps.

The heavy hitter of Material Design component libraries on React Native is react-native-paper, and this guide will focus on using react-native-paper to set up a starter app with the some of the most prominent and recognizable Material Design features: Hamburger Menu, Drawer Navigation, FAB (Floating Action Button), and Contextual Action Bar.

Sections

Demo

This is what the starter app I’m going to build will eventually look like. All the code for this demo is available in this GitHub repo: material-ui-in-react-native.

Setup

First, I’ll initialize my React Native app using Expo. You don’t have to use Expo, it just helps me get started, so I can focus on the UI in this example.

If you don’t have expo-cli installed, then first run:

npm install -g expo-cli 

Now run the following:

expo init material-ui-in-react-native -t expo-template-blank-typescript
cd material-ui-in-react-native
yarn add react-native-paper 
You can also follow these additional installation instructions to enable tree-shaking, reduce bundle-size, etc. with react-native-paper

I’m also adding react-navigation to this project. I recommend you use it as well. It’s the most popular navigation library for React Native, and there’s more support for running it alongside react-native-paper compared to other navigation libraries. Follow the installation instructions for react-navigation, since they are slightly different, depending on whether you use Expo or plain React Native.

Initial Screens

Create the following two files in your app’s main directory (if you want the styles used, remember, everything for this example is available in this GitHub repo):

MyFriends.tsx

import React from 'react';
import {View} from 'react-native';
import {Title} from 'react-native-paper';
import base from './styles/base';

interface IMyFriendsProps {}

const MyFriends: React.FunctionComponent<IMyFriendsProps> = (props) => {
  return (
    <View style={base.centered}>
      <Title>MyFriends</Title>
    </View>
  );
};

export default MyFriends;

Profile.tsx

import React from 'react';
import {View} from 'react-native';
import {Title} from 'react-native-paper';
import base from './styles/base';

interface IProfileProps {}

const Profile: React.FunctionComponent<IProfileProps> = (props) => {
  return (
    <View style={base.centered}>
      <Title>Profile</Title>
    </View>
  );
};

export default Profile;

Over the course of this guide, I’ll link these screens to each other using a Navigation Drawer (or Hamburger Menu) and add Material UI components to each of them.

Hamburger Menu / Drawer Navigation

Material Design promotes the usage of a Navigation Drawer, so I’ll use this type of UI to make the My Friends and Profile screens navigable to and from each other.

First, I’ll add React Navigation’s drawer library:

  yarn add @react-navigation/native @react-navigation/drawer

Now I’ll add the following into my App.tsx to enable Drawer Navigation. It should look like the following:

App.tsx

import React from 'react';
import {createDrawerNavigator} from '@react-navigation/drawer';
import {NavigationContainer} from '@react-navigation/native';
import {StatusBar} from 'expo-status-bar';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import MyFriends from './MyFriends';
import Profile from './Profile';

export default function App() {
  const Drawer = createDrawerNavigator();
  return (
    <SafeAreaProvider>
      <NavigationContainer>
        <Drawer.Navigator>
          <Drawer.Screen name='My Friends' component={MyFriends} />
          <Drawer.Screen name='Profile' component={Profile} />
        </Drawer.Navigator>
      </NavigationContainer>
      <StatusBar style='auto' />
    </SafeAreaProvider>
  );
}

This drawer also needs a button to open it. That button should look like the classic hamburger icon (≑) and it should open the navigation drawer when pressed. Here’s what that button might look like:

components/MenuIcon.tsx

import React from 'react';
import {IconButton} from 'react-native-paper';
import {DrawerActions, useNavigation} from '@react-navigation/native';
import {useCallback} from 'react';

export default function MenuIcon() {
  const navigation = useNavigation();

  const openDrawer = useCallback(() => {
    navigation.dispatch(DrawerActions.openDrawer());
  }, []);

  return <IconButton icon='menu' size={24} onPress={openDrawer} />;
}

A few things to notice here:

  1. React-navigation’s useNavigation hook is how we are going to execute most navigation actions, from changing screens to opening drawers.

  2. The <IconButton> component is from react-native-paper. It supports all the Material Design icons by name and optionally supports any ReactNode that you want to pass in there, which allows one to add in any desired icon from any third-party library.

Now I’ll add my <MenuIcon> to my Navigation Drawer by replacing this from App.tsx:

  <Drawer.Navigator>
    ...
  </Drawer.Navigator>

With the following:

import MenuIcon from './components/MenuIcon.tsx';
...
  <Drawer.Navigator
    screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}}
  >
    ...
  </Drawer.Navigator>

Lastly, I can customize my Navigation Drawer using the drawerContent prop of the same <Drawer.Navigator> component I just altered.

I’ll show an example which adds a header image to the top of the drawer. Feel free to customize with whatever you want to put in the drawer:

components/MenuContent.tsx

import React from 'react';
import {
  DrawerContentComponentProps,
  DrawerContentScrollView,
  DrawerItemList,
} from '@react-navigation/drawer';
import {Image} from 'react-native';

const MenuContent: React.FunctionComponent<DrawerContentComponentProps> = (
  props
) => {
  return (
    <DrawerContentScrollView {...props}>
      <Image
        resizeMode='cover'
        style={{width: '100%', height: 140}}
        source={require('../assets/drawerHeaderImage.jpg')}
      />
      <DrawerItemList {...props} />
    </DrawerContentScrollView>
  );
};
export default MenuContent;

Now I’ll pass <MenuContent> into <Drawer.Navigator>. To do this, I’ll make the following change in App.tsx from this:

import MenuIcon from './components/MenuIcon.tsx';
...
  <Drawer.Navigator
    screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}}
  >
    ...
  </Drawer.Navigator>

to this:

import MenuIcon from './components/MenuIcon.tsx';
import MenuContent from './components/MenuContent.tsx';
...
  <Drawer.Navigator
    screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}}
    drawerContent={(props) => <MenuContent {...props} />}
  >
    ...
  </Drawer.Navigator>

And now, I have fully functioning Drawer Navigation with a custom image header. Here’s the result:

Next, I’ll flesh out the main screens with more Material Design concepts.

Floating Action Button (FAB)

One of the hallmarks of Material Design is the Floating Action Button (or FAB). The <FAB> and <FAB.Group> components provide a useful implementation of the Floating Action Button according to Material Design principles. With minimal setup, I’ll add this to the My Friends screen right now.

First, I’ll need to add the <Provider> component from react-native-paper and wrap that component around the <NavigationContainer> in App.tsx as follows:

App.tsx

import {Provider} from 'react-native-paper';

...
  <Provider>
    <NavigationContainer>
      ...
    </NavigationContainer>
  </Provider>

Now I’ll add my Floating Action Button to the My Friends screen. I need

  • The <Portal> and <FAB.Group> components from react-native-paper
  • A state variable fabIsOpen to keep track of whether the FAB is open or closed
  • Some information about whether or not this screen is currently visible to the user (isScreenFocused). I need isScreenFocused because without it, I might end up with the FAB being visible on other screens than the My Friends screen

Here’s what the My Friends screen looks like with all that added in:

MyFriends.tsx

import {useIsFocused} from '@react-navigation/native';
import React, {useState} from 'react';
import {View} from 'react-native';
import {FAB, Portal, Title} from 'react-native-paper';
import base from './styles/base';

interface IMyFriendsProps {}

const MyFriends: React.FunctionComponent<IMyFriendsProps> = (props) => {
  const isScreenFocused = useIsFocused();
  const [fabIsOpen, setFabIsOpen] = useState(false);

  return (
    <View style={base.centered}>
      <Title>MyFriends</Title>
      <Portal>
        <FAB.Group
          visible={isScreenFocused}
          open={fabIsOpen}
          onStateChange={({open}) => setFabIsOpen(open)}
          icon={fabIsOpen ? 'close' : 'account-multiple'}
          actions={[
            {
              icon: 'plus',
              label: 'Add new friend',
              onPress: () => {},
            },
            {
              icon: 'file-export',
              label: 'Export friend list',
              onPress: () => {},
            },
          ]}
        />
      </Portal>
    </View>
  );
};

export default MyFriends;

Now the My Friends screen behaves like the following:

Next, I’ll add a Contextual Action Bar, which can be activated whenever an item in one of the screens is long-pressed.

Contextual Action Bar

Apps like Gmail and Google Photos make use of a Material Design concept called the Contextual Action Bar. I’ll implement a version of this quickly in the current app.

First, I’ll build the ContextualActionBar component itself using the Appbar component from react-native-paper. It should look something like this, to start with:

./components/ContextualActionBar.tsx

import React from 'react';
import {Appbar} from 'react-native-paper';

interface IContextualActionBarProps {}

const ContextualActionBar: React.FunctionComponent<IContextualActionBarProps> = (
  props
) => {
  return (
    <Appbar.Header {...props} style={{width: '100%'}}>
      <Appbar.Action icon='close' onPress={() => {}} />
      <Appbar.Content title='' />
      <Appbar.Action icon='delete' onPress={() => {}} />
      <Appbar.Action icon='content-copy' onPress={() => {}} />
      <Appbar.Action icon='magnify' onPress={() => {}} />
      <Appbar.Action icon='dots-vertical' onPress={() => {}} />
    </Appbar.Header>
  );
};

export default ContextualActionBar;

Now I want this component to render on top of the given screen’s header whenever an item is long pressed. Back in the My Friends screen, I’ve added some items for this purpose. On that screen, here’s how I’ll render the Contextual Action Bar over the screen’s header:

MyFriends.tsx

import {useNavigation} from '@react-navigation/native';
import ContextualActionBar from './components/ContextualActionBar';
...
  const [cabIsOpen, setCabIsOpen] = useState(false);
  const navigation = useNavigation();

  const openHeader = useCallback(() => {
    setCabIsOpen(!cabIsOpen);
  }, [cabIsOpen]);

  useEffect(() => {
    if (cabIsOpen) {
      navigation.setOptions({
        // have to use props: any since that's the type signature
        // from react-navigation...
        header: (props: any) => (<ContextualActionBar {...props} />),
      });
    } else {
      navigation.setOptions({header: undefined});
    }
  }, [cabIsOpen]);
...
  return (
    ...
    <List.Item
      title='Friend #1'
      description='Mar 18 | 3:31 PM'
      style={{width: '100%'}}
      onPress={() => {}}
      onLongPress={openHeader}
    />
    ...
  );

Above, I’m toggling a state boolean value (cabIsOpen) whenever a given item is long pressed. Based on that value, I either switch the React Navigation header to render the <ContextualActionBar> or switch back to render the default React Navigation header.

Now I should have a Contextual Action Bar appear when I long-press the “Friend #1” item. However, the title is still empty and I cannot do anything in any of the actions because the <ContextualActionBar> is unaware of any of the state of either the “Friend #1” item or the larger My Friends screen as a whole.

Thus, the next step is to add pass a title into the <ContextualActionBar> and pass in a function which can close the bar and be triggered by one of the buttons in the bar.

To do this, I have to add another state variable to the My Friends screen:

const [selectedItemName, setSelectedItemName] = useState('');

I also need to create a function which will close the header and reset the above state variable:

const closeHeader = useCallback(() => {
  setCabIsOpen(false);
  setSelectedItemName('');
}, []);

Then I need to pass both selectedItemName and closeHeader as props to <ContextualActionBar>:

useEffect(() => {
  if (cabIsOpen) {
    navigation.setOptions({
      header: (props: any) => (
        <ContextualActionBar
          {...props}
          title={selectedItemName}
          close={closeHeader}
        />
      ),
    });
  } else {
    navigation.setOptions({header: undefined});
  }
}, [cabIsOpen, selectedItemName]);

Lastly, I need to set selectedItemName to the title of the item that’s been long pressed:

const openHeader = useCallback((str: string) => {
  setSelectedItemName(str);
  setCabIsOpen(!cabIsOpen);
}, [cabIsOpen]);
...
return (
  ...
  <List.Item
    title='Friend #1'
    ...
    onLongPress={() => openHeader('Friend #1')}
  />
);

And now I can use the title and close props in <ContextualActionBar> as follows:

./components/ContextualActionBar.tsx

interface IContextualActionBarProps {
  title: string;
  close: () => void;
}
...
  return (
      ...
      <Appbar.Action icon='close' onPress={props.close} />
      <Appbar.Content title={props.title} />
      ...
  );

Now, I have a functional, Material Design-inspired Contextual Action Bar, utilizing react-native-paper and react-navigation, which looks like the following:

Contextual Action Bar, activates when the user long presses an item

Theming

The last thing I want to do is theme my app so I can change the primary color, secondary color, text colors, etc.

Theming is a little tricky because both react-navigation and react-native-paper have their own ThemeProvider components, and they can easily conflict with each other. Fortunately, there’s a great guide available on how to theme an app which uses both react-native-paper and react-navigation. If you follow this, you should be all set to go.

I’ll add in a little extra help for those who use Typescript and would run into esoteric errors trying to follow the above guide.

First, I’ll create a theme file which looks like the following. A few things to note are:

  • The return type of combineThemes encompasses both ReactNavigationTheme and ReactNativePaper.Theme
  • I changed the primary and accent colors, which will affect the CAB and FAB respectively
  • I added a new color to the theme called animationColor. If you don’t want to add a new color, you don’t need to declare the global namespace

theme.ts

import {
  DarkTheme as NavigationDarkTheme,
  DefaultTheme as NavigationDefaultTheme,
  Theme,
} from '@react-navigation/native';
import {ColorSchemeName} from 'react-native';
import {
  DarkTheme as PaperDarkTheme,
  DefaultTheme as PaperDefaultTheme,
} from 'react-native-paper';

declare global {
  namespace ReactNativePaper {
    interface ThemeColors {
      animationColor: string;
    }
    interface Theme {
      statusBar: 'light' | 'dark' | 'auto' | 'inverted' | undefined;
    }
  }
}

interface ReactNavigationTheme extends Theme {
  statusBar: 'light' | 'dark' | 'auto' | 'inverted' | undefined;
}

export function combineThemes(
  themeType: ColorSchemeName
): ReactNativePaper.Theme | ReactNavigationTheme {
  const CombinedDefaultTheme: ReactNativePaper.Theme = {
    ...NavigationDefaultTheme,
    ...PaperDefaultTheme,
    statusBar: 'dark',
    colors: {
      ...NavigationDefaultTheme.colors,
      ...PaperDefaultTheme.colors,
      animationColor: '#2922ff',
      primary: '#079c20',
      accent: '#2922ff',
    },
  };
  const CombinedDarkTheme: ReactNativePaper.Theme = {
    ...NavigationDarkTheme,
    ...PaperDarkTheme,
    mode: 'adaptive',
    statusBar: 'light',
    colors: {
      ...NavigationDarkTheme.colors,
      ...PaperDarkTheme.colors,
      animationColor: '#6262ff',
      primary: '#079c20',
      accent: '#2922ff',
    },
  };

  return themeType === 'dark' ? CombinedDarkTheme : CombinedDefaultTheme;
}

Then, back in App.tsx I’ll add my theme to both the react-native-paper Provider component and the NavigationContainer component from react-navigation as follows:

App.tsx

import {useColorScheme} from 'react-native';
import {NavigationContainer, Theme} from '@react-navigation/native';
import {combineThemes} from './theme';
...
  const colorScheme = useColorScheme() as 'light' | 'dark';
  const theme = combineThemes(colorScheme);
  ...
    <Provider theme={theme as ReactNativePaper.Theme}>
      <NavigationContainer theme={theme as Theme}>
      </NavigationContainer>
    </Provider>
  ...

I am using Expo, so I additionally need to add the following in app.json to enable dark mode. You may not need to:

"userInterfaceStyle": "automatic",

And now, a custom-themed, dark-mode-enabled, Material Design-inspired app! It looks great!

Contextual Action Bar and Floating Action Button with custom colors, light theme

Drawer open, showing header image, light mode

Contextual Action Bar and Floating Action Button with custom colors, dark theme

Drawer open, showing header image, dark mode

Conclusion

If you followed along to the end with me here, then you should have your own cross-platform app with Material Design elements from the react-native-paper library like Drawer Navigation (with custom designs in the drawer menu), Floating Action Buttons, and Contextual Action Bars. You should also have theming enabled which plays nicely with both the react-native-paper and react-navigation libraries. This setup should enable you to quickly and stylishly build out your next mobile app with ease.

Farhan Kathawala
Farhan Kathawala
Full Stack Web / React Native Developer

A happy full stack developer sharing tidbits of everything and anything web / mobile app dev.

Related