Updated June 14, 2022
How to Detect User Color Preference in React Native
React Native has two ways for you to determine a user's appearance preferences:
- The Appearance API which lets you get their current color scheme
- The useColorScheme hook which provides an up-to-date color scheme (it will automatically update as the user's preferences change)
Today we'll be leveraging the useColorScheme
hook.
We'll take a look at the following screen which shows black text and a white background - a light color scheme.
import React from "react"
import { View, Text, ViewStyle, TextStyle, StyleSheet } from "react-native"
const App = () => {
const viewStyles: ViewStyle[] = [
styles.container,
{ backgroundColor: "white" },
]
const textStyles: TextStyle[] = [styles.text, { color: "black" }]
return (
<View style={viewStyles}>
<Text style={textStyles}>Hello, world!</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
text: {
fontWeight: "bold",
fontSize: 20,
},
})
export default App
We can use the useColorScheme
hook and determine the user's preferred color scheme, changing our colors based on it.
import React from "react"
import {
View,
Text,
ViewStyle,
TextStyle,
StyleSheet,
useColorScheme,
} from "react-native"
const App = () => {
const colorScheme = useColorScheme()
const isLightTheme = colorScheme === "light"
const viewStyles: ViewStyle[] = [
styles.container,
{ backgroundColor: isLightTheme ? "white" : "black" },
]
const textStyles: TextStyle[] = [
styles.text,
{ color: isLightTheme ? "black" : "white" },
]
return (
<View style={viewStyles}>
<Text style={textStyles}>Hello, world!</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
text: {
fontWeight: "bold",
fontSize: 20,
},
})
export default App
NOTE: If you have the chrome debugger enabled
useColorScheme
will always return 'light'.
Improving Reusability with useColorScheme
Currently our component needs to know the color theme to determine what color to use. We can put our app colors into a Colors
object where we define colors based on the theme.
Then, in the component, we can just say we want to use colors.background
or colors.text
. Something descriptive.
import React from "react"
import {
View,
Text,
ViewStyle,
TextStyle,
StyleSheet,
useColorScheme,
} from "react-native"
const Colors = {
light: {
background: "white",
text: "black",
},
dark: {
background: "black",
text: "white",
},
}
const App = () => {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme]
const viewStyles: ViewStyle[] = [
styles.container,
{ backgroundColor: colors.background },
]
const textStyles: TextStyle[] = [styles.text, { color: colors.text }]
return (
<View style={viewStyles}>
<Text style={textStyles}>Hello, world!</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
text: {
fontWeight: "bold",
fontSize: 20,
},
})
export default App
TypeScript Tip: Dealing with useColorScheme null Type
You may be seeing a warning when trying to access Colors[colorScheme]
because colorScheme
may be null
...
This is a nice tip I saw in an Expo Template on how to fix this.
We can create a custom useColorScheme
hook that changes the return type to not be null via NonNullable
.
// hooks/useColorScheme.ts
import {
ColorSchemeName,
useColorScheme as _useColorScheme,
} from "react-native"
// The useColorScheme value is always either light or dark, but the built-in
// type suggests that it can be null. This will not happen in practice, so this
// makes it a bit easier to work with.
export default function useColorScheme(): NonNullable<ColorSchemeName> {
return _useColorScheme() as NonNullable<ColorSchemeName>
}
Creating your own useColorScheme
hook is also nice if you want to "fake" a different color scheme/quickly change through them. Rather than returning the result of _useColorScheme
you can just return dark
or light
.
Custom useThemeColors Hook
Finally, in an effort to write even more reusable code, we can simplify our logic further and create a useThemeColors
hook to return just the colors of the current theme without the component having to be aware of what the current theme is.
// hooks/useThemeColors.ts
import useColorScheme from "hooks/useColorScheme"
const Colors = {
light: {
background: "white",
text: "black",
},
dark: {
background: "black",
text: "white",
},
}
const useThemeColors = () => {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme]
return colors
}
export default useThemeColors
Further reading: how to set up path alias', like shown above, in a React Native + TypeScript project.
Now your App.tsx
can be as simple as
import React from "react"
import { View, Text, ViewStyle, TextStyle, StyleSheet } from "react-native"
import useThemeColors from "hooks/useThemeColors"
const App = () => {
const colors = useThemeColors()
const viewStyles: ViewStyle[] = [
styles.container,
{ backgroundColor: colors.background },
]
const textStyles: TextStyle[] = [styles.text, { color: colors.text }]
return (
<View style={viewStyles}>
<Text style={textStyles}>Hello, world!</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
text: {
fontWeight: "bold",
fontSize: 20,
},
})
export default App
Now within our component we don't have to do anything to figure out the current theme or which colors to use for that them. The useThemeColors
hook encapsulates all of that and returns a color object from which we can choose the right color for our UI elements.
Expanded Example
You can find an expanded version of this code in a more real-world example in React Native School's Clock example app.