Fixing React Native RefreshControl Color
If you’ve ever used RefreshControl in React Native on iOS, you might have noticed that the spinner color doesn’t always match what you set with tintColor. In this article, I’ll explain the problem and share a simple workaround.
The Problem
On iOS, RefreshControl wraps the native UIRefreshControl, which renders an ActivityIndicatorView. Without a custom tintColor, the system uses a semantic default color based on the device’s appearance mode:
- Light mode: dark gray spinner
- Dark mode: light gray spinner
This is fine if you’re okay with the default. But when you set tintColor="white" for example, you’d expect the spinner to always be white. Unfortunately, that’s not always the case.
The tintColor prop is intermittently ignored, especially on the first render. This is a known bug related to the New Architecture (Fabric) and has been reported multiple times on the React Native GitHub:
- #53987 —
tintColorprop not respected (RN 0.81.4, still open) - #48502 —
tintColorsometimes doesn’t work on first render (RN 0.76.3) - #46631 —
tintColornot working in ScrollView (RN 0.75.2)
There have been fixes merged, but the issue keeps resurfacing in newer versions. If you’re hitting this bug, here’s a reliable workaround.
The Workaround
The idea is simple: start with a transparent color, then apply the actual color after a short delay using setTimeout. This bypasses the native rendering timing issue where the tintColor gets overwritten during the initial mount.
import { useEffect, useState } from "react";
import { RefreshControl as RNRefreshControl } from "react-native";
export const RefreshControl = (
props: React.ComponentProps<typeof RNRefreshControl>,
) => {
const selectedColor = "white";
const [refreshColor, setRefreshColor] = useState("transparent");
useEffect(() => {
const timer = setTimeout(() => setRefreshColor(selectedColor), 50);
return () => clearTimeout(timer);
}, []);
return (
<RNRefreshControl
tintColor={refreshColor}
colors={[refreshColor]}
{...props}
/>
);
};
A few things to note:
- We initialize
refreshColorto"transparent"so the spinner is invisible during the problematic first render. - The
setTimeoutwith a 50ms delay is enough to bypass the native rendering issue. - We also set
colors(Android prop) to the same value for consistency across platforms. - All other props are passed through via
{...props}, so it works as a drop-in replacement.
Usage
Use it just like the regular RefreshControl:
import { FlatList } from "react-native";
import { RefreshControl } from "./components/RefreshControl";
<FlatList
data={data}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
renderItem={renderItem}
/>
Why This Works
On iOS, the native UIRefreshControl seems to reset or ignore the tintColor during the initial component mount in the New Architecture. By delaying the color application slightly, we ensure the prop is set after the native component has fully initialized. The 50ms delay is imperceptible to users since the spinner isn’t typically visible at mount time anyway.
It’s a bit hacky, but it works reliably. Hopefully the React Native team will fix this properly in a future release.