React Native Theme Toggle With Lottie & Material-UI
Hey guys! Ever found yourself wrestling with themes and animations in React Native, trying to make everything look slick and consistent across your app? Today, we're diving deep into how to implement a type-safe, theme-aware Lottie animation toggle using TypeScript, React Native, and Material-UI (v5). Imagine a cool rope-pull animation that smoothly switches between light and dark modes – that’s the kind of finesse we’re aiming for. By August 2025, this approach will be even more crucial as apps become increasingly polished and user expectations rise. So, buckle up and let's get started!
Project Setup and Dependencies
First things first, let’s lay the groundwork. We’ll need to set up a React Native project with TypeScript, Material-UI, and Lottie. If you’ve already got a project running, awesome! If not, no sweat – we’ll walk through it. Make sure you've got Node.js and npm (or yarn) installed. We’ll be using npx
to create our React Native project.
npx react-native init AwesomeThemeToggle --template react-native-template-typescript
cd AwesomeThemeToggle
This command bootstraps a new React Native project with TypeScript configured. Next, we’ll install the necessary dependencies:
yarn add @mui/material @emotion/react @emotion/styled react-native-vector-icons lottie-react-native @types/lottie-react-native
# or
npm install @mui/material @emotion/react @emotion/styled react-native-vector-icons lottie-react-native @types/lottie-react-native
Here’s a quick rundown of what each package brings to the table:
- @mui/material: Material-UI components for React Native.
- @emotion/react and @emotion/styled: Emotion is a CSS-in-JS library that integrates beautifully with Material-UI.
- react-native-vector-icons: Icons for our toggle (optional but highly recommended).
- lottie-react-native: Lottie for rendering our animations.
- @types/lottie-react-native: TypeScript type definitions for Lottie.
Diving Deep into Project Configuration
Now that we've got our dependencies installed, let's tweak the project configuration a bit to ensure everything plays nicely together. First, you'll want to link the vector icons library. React Native often requires linking native modules, especially for libraries that include native code or assets. For react-native-vector-icons
, this is usually a straightforward process, but it can vary slightly depending on your React Native version and platform. Generally, you'll use the react-native link
command, but with the newer versions of React Native, auto-linking is often enabled, which simplifies this step. However, it's always good to double-check the library's documentation for the most accurate instructions, as these can change over time. If you encounter any issues, the documentation usually has troubleshooting steps that can help you resolve common problems like missing fonts or incorrect paths. Remember, a smooth setup process is crucial for a seamless development experience, so taking the time to ensure everything is correctly configured upfront can save you headaches down the line.
Next up, we'll tackle the TypeScript configuration. TypeScript is a superpower for React Native projects, bringing static typing and improved tooling to the table. To make the most of it, we need to ensure our tsconfig.json
file is set up optimally. This file tells the TypeScript compiler how to handle our code, what rules to enforce, and how to output the compiled JavaScript. Key settings to consider include target
, which specifies the ECMAScript target version; jsx
, which tells TypeScript how to handle JSX syntax; and moduleResolution
, which dictates how modules are resolved. We'll also want to define include and exclude patterns to specify which files should be included in the compilation process. A well-configured tsconfig.json
file not only helps catch errors early but also improves code readability and maintainability, making it an indispensable part of our development workflow. Plus, it’s great for future-proofing your project as TypeScript evolves, ensuring you can leverage new features and improvements as they become available. So, spending a bit of time fine-tuning your TypeScript configuration is an investment that pays off in the long run, both in terms of code quality and developer productivity.
Setting Up Material-UI Theme
With our project dependencies in place, let's set up our Material-UI theme. This is where the magic happens for theme switching. We’ll create a theme.ts
file (or .tsx
if you prefer) to define our light and dark themes. This file will house the color palettes, typography, and other theme-related configurations that Material-UI components will use to style themselves.
// src/theme.ts
import { createTheme, Theme } from '@mui/material/styles';
declare module '@mui/material/styles' {
interface Theme {
status: {
danger: string;
};
}
interface ThemeOptions {
status?: {
danger?: string;
};
}
}
export const lightTheme: Theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#6200EE',
},
secondary: {
main: '#03DAC6',
},
background: {
default: '#FFFFFF',
paper: '#F5F5F5',
},
text: {
primary: '#000000',
secondary: '#757575',
},
},
status: {
danger: '#e53e3e',
},
});
export const darkTheme: Theme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#BB86FC',
},
secondary: {
main: '#03DAC6',
},
background: {
default: '#121212',
paper: '#1E1E1E',
},
text: {
primary: '#FFFFFF',
secondary: '#B0BEC5',
},
},
status: {
danger: '#ff9800',
},
});
Deep Dive into Theme Customization
In this theme.ts
file, we're defining two distinct themes: lightTheme
and darkTheme
. Each theme is created using Material-UI's createTheme
function, which allows us to customize various aspects of the UI. The most important part is the palette
, which dictates the color scheme of our app. We've set up primary, secondary, background, and text colors for both light and dark modes. Notice how the mode
property is set to either 'light'
or 'dark'
, which is a crucial setting for Material-UI to understand which theme to apply. The primary and secondary colors are the main accent colors used throughout your application, while the background and text colors define the overall look and feel. The background is further divided into default
(the main background color) and paper
(the color for paper-like components like cards and dialogs).
But that's not all! We've also extended the theme's type definitions. By declaring a module for '@mui/material/styles'
, we can add custom properties to the Theme
and ThemeOptions
interfaces. In our case, we've added a status
property with a danger
color. This is a neat trick to add your own theme-specific values that aren't part of the standard Material-UI theme. This can be incredibly useful for things like alert colors, special indicators, or any other custom styling you might need. By extending the theme's type definitions, we ensure that TypeScript knows about our custom properties, providing type safety and autocompletion throughout our codebase. This is a powerful way to make your theme more expressive and tailored to your application's needs. So, don't be afraid to dive into theme customization – it's where you can really make your app stand out and provide a unique user experience.
Creating a Theme Context
Next, we need a way to share our theme across the app. React’s Context API is perfect for this. Let’s create a ThemeContext
in src/context/ThemeContext.tsx
.
// src/context/ThemeContext.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';
import { Theme } from '@mui/material/styles';
import { lightTheme, darkTheme } from '../theme';
interface ThemeContextProps {
theme: Theme;
toggleTheme: () => void;
isDarkMode: boolean;
}
const ThemeContext = createContext<ThemeContextProps>({
theme: lightTheme,
toggleTheme: () => {},
isDarkMode: false,
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(false);
const theme = isDarkMode ? darkTheme : lightTheme;
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme, isDarkMode }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
Unpacking the Theme Context
In this snippet, we're crafting a ThemeContext
that serves as the backbone for our theme-switching mechanism. Let's break down what's happening step by step. First, we import the necessary modules from React, including createContext
, useState
, useContext
, and ReactNode
. These are the building blocks we'll use to create our context and manage the theme state. We also import the Theme
type from Material-UI and our lightTheme
and darkTheme
objects that we defined earlier.
Next, we define an interface called ThemeContextProps
. This interface specifies the shape of the values that will be provided by our context. It includes three properties: theme
, which is the current Material-UI theme object; toggleTheme
, which is a function that toggles between light and dark themes; and isDarkMode
, which is a boolean indicating whether the app is currently in dark mode. By defining this interface, we ensure that our context is type-safe, meaning TypeScript will help us catch errors if we try to access properties that don't exist or use them in the wrong way. This is a huge win for maintainability and helps prevent runtime bugs.
Then, we create the ThemeContext
itself using createContext
. We provide a default value for the context, which includes the lightTheme
, an empty toggleTheme
function, and isDarkMode
set to false
. This default value is only used if a component tries to access the context outside of a ThemeProvider
, which is a safety net to prevent unexpected behavior. Now, let's talk about the ThemeProvider
component. This is the heart of our context. It's a React functional component that accepts children
as props, allowing us to wrap any part of our app that needs access to the theme. Inside ThemeProvider
, we use the useState
hook to manage the isDarkMode
state. This state determines which theme is currently active (lightTheme
or darkTheme
). The toggleTheme
function is a simple state updater that toggles the value of isDarkMode
whenever it's called. Finally, we return a ThemeContext.Provider
component. This component makes the context values available to all its children. We pass an object as the value
prop, which includes the current theme
, the toggleTheme
function, and the isDarkMode
flag. This is the data that will be accessible to any component that uses our context.
Wrapping the App with ThemeProvider
Now, let’s wrap our app with the ThemeProvider
in App.tsx
.
// App.tsx
import React from 'react';
import { StatusBar } from 'react-native';
import { ThemeProvider } from './src/context/ThemeContext';
import Main from './src/Main';
const App: React.FC = () => {
return (
<ThemeProvider>
<StatusBar barStyle="light-content" />
<Main />
</ThemeProvider>
);
};
export default App;
The Role of App Wrapping in Theme Provision
Wrapping the App
component with the ThemeProvider
is a pivotal step in ensuring that our entire application can access and utilize the theme we've meticulously crafted. Think of the ThemeProvider
as a protective layer, a benevolent guardian that envelops our app and imbues it with the power to switch between light and dark modes seamlessly. This wrapping isn't just a technicality; it's a strategic move that fundamentally shapes how our components interact with the theme. By placing the ThemeProvider
at the root of our component tree, we establish a hierarchical structure where the theme context becomes universally available. This means that any component, no matter how deeply nested within the application, can tap into the theme and adapt its appearance accordingly. It's like setting up a central hub for theme information, a single source of truth that all parts of the app can rely on.
But why is this so important? Well, consider the alternative. Without this central provision, we'd be forced to manually pass the theme and toggle function down through multiple layers of components. This would not only be tedious and error-prone but also create a brittle system that's difficult to maintain. Imagine having to update the theme prop in every component that uses it whenever we make a change – a developer's nightmare! The ThemeProvider
elegantly sidesteps this issue by abstracting away the complexity of theme management. It allows us to focus on building our UI, confident that the theme will be readily available wherever it's needed. This approach also promotes a cleaner, more modular codebase. Components don't need to be aware of how the theme is managed; they simply consume it through the useTheme
hook. This decoupling makes our components more reusable and easier to test, as they're not tightly coupled to the theme implementation. So, wrapping the App
with ThemeProvider
is not just a convenience; it's a cornerstone of our application's architecture, setting the stage for a theme-aware, maintainable, and delightful user experience.
Creating the Main Component
Let’s create a Main
component (src/Main.tsx
) to hold our toggle and other UI elements.
// src/Main.tsx
import React from 'react';
import { View, StyleSheet, SafeAreaView } from 'react-native';
import { useTheme } from './src/context/ThemeContext';
import { IconButton } from '@mui/material';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import LottieView from 'lottie-react-native';
// Import your Lottie animation JSON file
import animation from './src/assets/lottie/rope_pull.json';
const Main: React.FC = () => {
const { theme, toggleTheme, isDarkMode } = useTheme();
const animationRef = React.useRef<LottieView>(null);
React.useEffect(() => {
if (animationRef.current) {
if (isDarkMode) {
animationRef.current.play(60, 100);
} else {
animationRef.current.play(0, 60);
}
}
}, [isDarkMode]);
const handleToggleTheme = () => {
toggleTheme();
if (animationRef.current) {
if (isDarkMode) {
animationRef.current.play(0, 60);
} else {
animationRef.current.play(60, 100);
}
}
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme.palette.background.default }]}>
<View style={[styles.container, { backgroundColor: theme.palette.background.default }]}>
<IconButton onPress={handleToggleTheme}>
<LottieView
ref={animationRef}
source={animation}
style={styles.animation}
autoPlay={false}
loop={false}
speed={1}
/>
</IconButton>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
animation: {
width: 200,
height: 200,
},
});
export default Main;
Dissecting the Main Component: The Heart of Our Theme Toggle
The Main
component is where the magic truly happens. It's the stage where our theme context, Lottie animation, and Material-UI components come together to create the interactive theme toggle. Let's dissect this component piece by piece to understand its inner workings.
First and foremost, we import the necessary modules from React Native, React, Material-UI, react-native-vector-icons
, and lottie-react-native
. These are our essential tools for building the UI and handling the animation. We also import the useTheme
hook from our custom ThemeContext
, which allows us to access the current theme, the toggleTheme
function, and the isDarkMode
flag. This is how our component becomes theme-aware.
Next, we import the Lottie animation JSON file. This file contains the animation data for our rope-pull toggle. Make sure you have a Lottie animation file (rope_pull.json
in this case) in your src/assets/lottie
directory. You can find a plethora of free Lottie animations on websites like LottieFiles. The animation reference is a crucial piece of the puzzle. We use React.useRef
to create a mutable reference to the LottieView
component. This allows us to control the animation programmatically, which is essential for our toggle functionality. The useEffect
hook is where we handle the initial animation playback based on the current theme. When the component mounts (or when isDarkMode
changes), this hook runs. Inside the hook, we check if the animationRef.current
is valid (i.e., the LottieView
has been mounted). If it is, we call animationRef.current.play
to start the animation. The play
function takes two arguments: the start frame and the end frame. By playing different segments of the animation based on isDarkMode
, we create the visual effect of toggling between light and dark modes. For example, we play from frame 60 to 100 when switching to dark mode and from frame 0 to 60 when switching to light mode. This creates a smooth transition that visually represents the theme change. The handleToggleTheme
function is the event handler for our toggle button. When the button is pressed, this function is called. First, we call toggleTheme
to update the theme in our context. Then, we use the same logic as in the useEffect
hook to play the appropriate segment of the Lottie animation. This ensures that the animation is synchronized with the theme change.
Finally, we return the JSX for our component. We wrap everything in a SafeAreaView
to ensure that our UI is rendered within the safe area of the screen (i.e., it doesn't overlap with the status bar or navigation bar). Inside the SafeAreaView
, we have a View
that serves as the main container. We apply styles to both the SafeAreaView
and the View
to set the background color based on the current theme. The star of the show is the IconButton
from Material-UI. This is our toggle button. We set its onPress
prop to handleToggleTheme
, so the function is called when the button is pressed. Inside the IconButton
, we have the LottieView
component. This component renders our Lottie animation. We set its ref
prop to animationRef
, allowing us to control it programmatically. We also set source
to our animation JSON file, autoPlay
to false
(because we want to control the animation manually), loop
to false
(because we only want the animation to play once per toggle), and speed
to 1
(for normal playback speed). And that's it! We've created a fully functional, theme-aware Lottie animation toggle in React Native. By dissecting the Main
component in this way, we can appreciate the interplay between the different technologies and techniques we've used. The result is a smooth, visually appealing, and type-safe theme toggle that enhances the user experience of our app.
Importing the Animation
Make sure you have a Lottie animation JSON file (e.g., rope_pull.json
) in your project. You can find many free Lottie animations on websites like LottieFiles. Place the file in a suitable directory, such as src/assets/lottie
, and import it into your Main.tsx
component.
// src/Main.tsx
import animation from './src/assets/lottie/rope_pull.json';
The Significance of Animation Import and Management
Importing the Lottie animation into our Main.tsx
component is a seemingly simple step, but it carries significant weight in the overall design and functionality of our theme toggle. This import is not just about adding a visual element; it's about injecting life and dynamism into our user interface. The animation serves as a visual cue, a delightful piece of feedback that confirms the user's action and enhances the sense of interactivity. When a user taps the toggle, they don't just see a switch flip; they witness a captivating animation that smoothly transitions between light and dark modes. This subtle yet powerful detail elevates the user experience, making the app feel more polished and responsive.
But the significance of animation import goes beyond mere aesthetics. It also touches on performance and resource management. Lottie animations are vector-based, which means they scale seamlessly across different screen sizes and resolutions without losing quality. This is a crucial advantage in the diverse landscape of mobile devices. Furthermore, Lottie animations are typically much smaller in file size compared to traditional image or video animations. This translates to faster loading times and reduced bandwidth consumption, especially important for users with limited data plans or slower internet connections. The way we import and manage the animation also plays a role in performance. By importing the animation JSON file directly into our component, we're leveraging the power of our bundler (like Metro in React Native) to optimize the asset loading process. The bundler can efficiently package the animation data with our JavaScript code, ensuring that it's readily available when the component is rendered. This approach avoids the overhead of fetching the animation from a remote server or parsing it at runtime, both of which can impact performance. So, the simple act of importing the animation is, in fact, a carefully considered decision that balances visual appeal, performance, and resource efficiency. It's a testament to the importance of paying attention to the small details that collectively contribute to a great user experience. By choosing Lottie animations and managing their import effectively, we're not just adding eye candy; we're investing in the overall quality and responsiveness of our application.
Running the App
Finally, let’s run our app! In your terminal, navigate to your project directory and run:
yarn ios # or yarn android
# or
npm run ios # or npm run android
And there you have it! A type-safe, theme-aware Lottie animation toggle in React Native with Material-UI. This approach not only enhances the visual appeal of your app but also ensures a consistent and user-friendly experience across different themes. By August 2025, techniques like these will be essential for creating top-notch mobile applications. Keep experimenting and happy coding, guys!
Conclusion
Wrapping up, what we’ve accomplished here is pretty awesome! We've walked through the entire process of implementing a type-safe, theme-aware Lottie animation toggle in a React Native application using Material-UI. This isn't just about making our app look cool; it's about creating a user experience that feels polished, consistent, and delightful. By leveraging TypeScript, we've ensured that our code is robust and maintainable, catching potential errors early in the development process. Material-UI has provided us with a set of pre-built components and a powerful theming system, making it easier to create a visually appealing and consistent UI. And Lottie animations have added that extra touch of finesse, making the theme transition feel smooth and engaging.
But the real beauty of this approach lies in its flexibility and scalability. The techniques we've learned can be applied to a wide range of scenarios beyond just theme toggles. We can use Lottie animations to create loading indicators, interactive elements, and other visual enhancements that elevate the user experience. The theme context pattern we've established can be extended to manage other application-wide settings, such as language preferences or accessibility options. And by embracing TypeScript, we've laid a solid foundation for future development, making it easier to add new features and maintain our codebase as our application grows. As we look ahead to August 2025 and beyond, the importance of these techniques will only continue to grow. Users are becoming increasingly sophisticated and demanding, expecting mobile applications to be not only functional but also visually appealing and intuitive. By mastering the art of themeing, animation, and type safety, we're equipping ourselves with the tools we need to meet these expectations and create truly exceptional user experiences.
So, don't stop here! Take what you've learned and experiment with different animations, themes, and UI patterns. Explore the vast world of LottieFiles and find animations that resonate with your app's brand and style. Dive deeper into Material-UI's theming capabilities and create custom themes that reflect your unique vision. And most importantly, keep coding, keep learning, and keep pushing the boundaries of what's possible in mobile app development. The future is bright, and with the right skills and techniques, we can all create amazing things.