Four ways to draw a gradient
| Approach | Install | Expo Go | Best for | Note |
|---|---|---|---|---|
| expo-linear-gradient | npx expo install | ✅ Yes | Any Expo project — the default | start/end vectors, no angle prop |
| react-native-linear-gradient | npm + pod install | ❌ Dev build | Bare RN CLI projects | Has useAngle / angle in degrees |
| backgroundImage style | Built in (newer RN) | ⚠️ Version-gated | Simple backgrounds, no extra dep | Check your RN version first |
| react-native-svg | npx expo install | ✅ Yes | Gradients inside shapes and paths | Overkill for a plain background |
The decision is almost always made for you by your project type. If you used create-expo-app, install expo-linear-gradient and stop reading this section. The community package is for bare React Native CLI projects, and the two are not interchangeable — installing both is a common accident that produces confusing native build errors.
The setup, complete
npx expo install expo-linear-gradientUse expo install rather than npm install so the version matches your SDK. There is no config plugin and nothing to add to app.json — it works in Expo Go immediately.
import { LinearGradient } from 'expo-linear-gradient';
import { StyleSheet, Text } from 'react-native';
export default function UpgradeCard() {
return (
<LinearGradient
colors={['#fb923c', '#ea580c']}
start={{ x: 0, y: 0 }} // top-left
end={{ x: 1, y: 1 }} // bottom-right
style={styles.card}
>
<Text style={styles.title}>Go Pro</Text>
<Text style={styles.sub}>Unlimited projects, priority builds</Text>
</LinearGradient>
);
}
const styles = StyleSheet.create({
// borderRadius belongs HERE, not on a wrapper View
card: { borderRadius: 20, padding: 24, gap: 6 },
title: { color: '#fff', fontSize: 22, fontWeight: '800' },
sub: { color: 'rgba(255,255,255,0.85)', fontSize: 14 },
});LinearGradient is a View with a painted background, so it accepts every layout style you already know and renders children on top of the gradient. That is the whole mental model: you are not applying a gradient to a box, you are using a box that happens to be a gradient.
Four gotchas, in the order you will hit them
1. The TypeScript error about tuples
Recent versions type colors as a tuple requiring at least two entries, so passing a plain string[] — very common when the colours come from a theme object — fails to compile with a message about readonly [ColorValue, ColorValue, ...ColorValue[]]. The fix is to preserve the tuple shape:
// ❌ theme.brandGradient is string[] — widened, won't compile
<LinearGradient colors={theme.brandGradient} />
// ✅ declare it as a tuple at the source
export const theme = {
brandGradient: ['#fb923c', '#ea580c'] as const,
};
// ✅ or assert at the call site
<LinearGradient colors={theme.brandGradient as [string, string]} />2. Rounded corners leaking on Android
If you wrap the gradient in a rounded View, Android will happily paint the gradient’s square corners over it while iOS looks correct — which is why this ships to production and gets reported a week later. Put borderRadius on the LinearGradient itself. If a wrapper is unavoidable because it owns the shadow, add overflow: 'hidden' to it.
3. Angles are relative to the box
start and end are fractions of the element, not screen coordinates, so {x:0,y:0} → {x:1,y:1} is corner-to-corner rather than a true 45°. On a wide, short banner that reads as almost horizontal. The useful presets:
// vertical (the default)
start={{ x: 0, y: 0 }} end={{ x: 0, y: 1 }}
// horizontal
start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}
// corner to corner
start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }}
// three stops with explicit positions — locations must match colors length
colors={['#1c1c1c', '#2a1a10', '#fb923c']}
locations={[0, 0.6, 1]}A mismatched locations array — three colours, two stops — does not throw. It renders something slightly wrong and you spend twenty minutes tuning hex values that were never the problem.
4. Gradient text needs a mask
Text has color, and color takes one value. To fill glyphs with a gradient you draw the gradient and cut it to the shape of the text:
npx expo install @react-native-masked-view/masked-viewimport MaskedView from '@react-native-masked-view/masked-view';
import { LinearGradient } from 'expo-linear-gradient';
import { Text } from 'react-native';
export function GradientHeading({ children }) {
return (
<MaskedView maskElement={<Text style={s.text}>{children}</Text>}>
<LinearGradient
colors={['#fb923c', '#f43f5e']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
{/* invisible copy sets the size the gradient must fill */}
<Text style={[s.text, { opacity: 0 }]}>{children}</Text>
</LinearGradient>
</MaskedView>
);
}Two patterns worth stealing
A fade over an image, so white text stays legible on any photo. This is the gradient that earns its keep in nearly every app:
<View>
<Image source={{ uri: cover }} style={{ height: 220 }} />
<LinearGradient
colors={['transparent', 'rgba(0,0,0,0.85)']}
style={StyleSheet.absoluteFillObject}
pointerEvents="none"
/>
<Text style={s.overlayTitle}>{title}</Text>
</View>pointerEvents="none"matters — without it the overlay swallows taps meant for the card underneath, which shows up as “the top half of my list item is not pressable.”
Animating between gradients. Changing colors on every frame re-renders the native view and stutters. Stack two gradients and cross-fade instead:
const progress = useSharedValue(0); // 0 → 1
const topStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
<View>
<LinearGradient colors={COOL} style={StyleSheet.absoluteFillObject} />
<Animated.View style={[StyleSheet.absoluteFillObject, topStyle]}>
<LinearGradient colors={WARM} style={StyleSheet.absoluteFillObject} />
</Animated.View>
</View>Use fewer of them than the mockup has
The technical problems above are all solvable in an afternoon. The design problem is not: gradients read as “generated” the moment every card, every button, and every header has one. The apps that look expensive use a single gradient as an accent — one primary button, or one hero — against otherwise flat surfaces. If your screen has three gradients, two of them are noise.
You can test that quickly rather than argue about it. Describe the screen — “a paywall with one gradient upgrade button and flat feature rows” — and ShipNative generates it as real React Native running on your phone, gradient package and all. Look at it at arm’s length, then decide how many you actually want. See also dark mode — every gradient you add is a second pair of colours to maintain.