react-native-edge-fade
v0.2.0
Published
Smooth, customizable edge fading for React Native
Readme
react-native-edge-fade
Fade any edge of any view. Scroll lists that dissolve into the background, hero images that melt into the screen, horizontal strips that hint at more content off-screen — in one component, on iOS, Android, and Web.
A filter carousel whose edges dissolve, built with a single
EdgeFadeView.
<EdgeFadeView bottom={80} style={{ flex: 1 }}>
<ScrollView>{/* your content fades smoothly at the bottom */}</ScrollView>
</EdgeFadeView>That's it. Wrap any view, declare which edges to fade, and you get a native-quality gradient that respects rounded corners, animates on the UI thread (via Reanimated), and works without a single extra dependency.
Why edge-fade?
Implementing edge fades by hand means juggling MaskedView, multiple LinearGradient layers, native shaders, and platform-specific clipping just to avoid visible banding. This library hides all of that behind one declarative prop surface:
| You wanted | You'd otherwise reach for | With edge-fade |
| ----------------------------------------- | -------------------------------------------------- | ------------------------- |
| Scroll list that fades into a blur/image | @react-native-masked-view + LinearGradient | mode="mask" (default) |
| Carousel hinting at off-screen items | Stacked LinearGradient views with manual sizing | left + right props |
| Smooth gradient with no banding on Android | Custom AGSL shader + API gating + fallbacks | Built in (API 33+) |
| Rounded card with a faded bottom | Nested View + overflow: hidden + mask hacks | radius prop |
| Content frosting under a nav bar (progressive blur) | Hand-rolled blur + masked material layers | mode="blur" (iOS 13+ / Android 12+) |
| Animated fade tied to scroll position | Bridge-roundtrip prop updates | AnimatedEdgeFadeView |
What you get
- Three modes —
mask(true alpha fade, reveals what's behind),overlay(paints a color over content), andblur(true progressive blur, Apple-Music style) - Four edges, independently controlled — different
size,curve, andcolorper side - Five preset curves + full
cubicBezierandstopssupport - Per-pixel AGSL shaders on Android 13+ — zero banding, exact curve math, dithered
- iOS
CALayermask withkCGBlendModeDestinationIn— composes cleanly with rounded corners and continuous (squircle) curvature - Web via CSS
mask-image+mask-composite: intersect - Fabric / New Architecture —
codegenNativeComponent, no Paper - Optional Reanimated integration — UI-thread animated fades, no React re-renders
- Zero required dependencies beyond
react+react-native
Installation
yarn add react-native-edge-fade
# or
npm install react-native-edge-fadeiOS — install pods after adding the package:
cd ios && pod installRequires React Native New Architecture (Fabric). The Paper renderer is not supported.
Quick start
import { EdgeFadeView } from 'react-native-edge-fade';
// Mask — fade the bottom of a scrollable list
<EdgeFadeView bottom={80} style={{ flex: 1 }}>
<ScrollView>{/* content */}</ScrollView>
</EdgeFadeView>
// Overlay — fade left and right edges into a background color
<EdgeFadeView mode="overlay" left={120} right={120} color="#000" curve="gentle">
<ScrollView horizontal>{/* content */}</ScrollView>
</EdgeFadeView>Props
| Prop | Type | Default | Description |
| -------- | ------------------------------------- | ---------- | ---------------------------------------------------- |
| top | boolean \| number \| EdgeConfig | false | Top edge fade |
| bottom | boolean \| number \| EdgeConfig | false | Bottom edge fade |
| left | boolean \| number \| EdgeConfig | false | Left edge fade (physical, direction-independent) |
| right | boolean \| number \| EdgeConfig | false | Right edge fade (physical, direction-independent) |
| start | boolean \| number \| EdgeConfig | false | Logical leading edge — maps to left in LTR, right in RTL |
| end | boolean \| number \| EdgeConfig | false | Logical trailing edge — maps to right in LTR, left in RTL |
| size | number | 80 | Default fade depth (dp) for all active edges |
| curve | EdgeFadeCurve | 'smooth' | Default curve shape for all active edges |
| mode | 'mask' \| 'overlay' \| 'blur' | auto | Render mode; inferred from color when omitted |
| color | ColorValue | — | Overlay color, or optional frost veil color in blur mode (omit for pure blur) |
| blurRadius | number | 28 | Max blur radius (dp) at the outer edge, blur mode only |
| frostProgression | number | 1 | blur mode — fraction of the band over which the curve's blur envelope completes (clamped 0.05–1) |
| frostSaturation | number | 0.9 | blur mode — saturation grade on the blurred pixels. Android only |
| frostLift | number | 1.03 | blur mode — brightness grade on the blurred pixels. Android only |
| radius | number | — | Corner radius (dp). Use this instead of style.borderRadius |
| style | ViewStyle | — | Forwarded to the native view |
Edge prop forms
// boolean — enables edge at the global default size (80 dp)
<EdgeFadeView bottom />
// number — enables edge at an explicit size
<EdgeFadeView bottom={120} />
// EdgeConfig — full per-edge control
<EdgeFadeView bottom={{ size: 120, curve: 'sharp', color: '#000' }} />RTL — start / end
start and end are logical edge props that follow I18nManager.isRTL,
matching the behavior of marginStart/marginEnd:
// Fade always on the leading side, regardless of layout direction.
<EdgeFadeView start={80}>{/* … */}</EdgeFadeView>| Layout direction | start resolves to | end resolves to |
| ---------------- | ------------------- | ----------------- |
| LTR | left | right |
| RTL | right | left |
left and right remain physical and are useful when the edge must stay on a
specific side regardless of localisation (rare). When both a logical and a
physical prop target the same resolved side, the logical one wins.
EdgeConfig
type EdgeConfig = {
size?: number; // overrides global size for this edge
curve?: EdgeFadeCurve; // overrides global curve for this edge
color?: ColorValue; // per-edge overlay color (overlay mode)
};Render modes
mode="mask" (default when no color)
Attenuates the alpha of the wrapped content using DST_IN compositing. Content is fully
visible at the inner edge (alpha = 1) and transparent at the outer edge (alpha = 0).
Whatever is behind the component is revealed through the fade.
Use this for content over images, gradients, blur, video, or dynamic backgrounds.
<EdgeFadeView bottom={80} style={{ flex: 1 }}>
<ScrollView>{/* … */}</ScrollView>
</EdgeFadeView>mode="overlay" (default when color is set)
Paints a color gradient over the content from transparent (inner) to opaque (outer). The content itself does not become transparent.
Use this when the fade should blend content into a known solid background color.
<EdgeFadeView mode="overlay" bottom={80} color="#fff">
<ScrollView>{/* … */}</ScrollView>
</EdgeFadeView>mode="blur"
Blurs the wrapped content toward the enabled edges with a true progressive blur — the
Apple Music / iOS scroll-edge look. Under the hood each fade band composites a stack of
increasing-radius Gaussian blurs, cross-faded along the band, so the perceived blur radius ramps
continuously from sharp at the inner edge to the full blurRadius at the outer edge. Blur passes
are clipped to the fade strips, so cost scales with the band area, not the view size.
The curve shapes the entire blur progression, not just its onset: it's the envelope the
perceived radius follows from sharp at the inner edge to full blurRadius at the outer edge, so
editing the Bézier reshapes the whole ramp. frostProgression (default 1) is the fraction of
the band (inner→outer) over which that envelope completes — smaller values compress the same
shape toward the inner edge, reaching full blur sooner and holding it for the rest of the band.
The band extent is set only by top / bottom / left / right.
blurRadius sets the maximum blur depth (dp) reached at the outer edge. color optionally adds
a frosted material veil on top of the blur (a smoothstep ramp to 0.6 max opacity) — omit it for
a pure, tint-free Gaussian blur. On Android, frostSaturation / frostLift grade the blurred
pixels toward a soft pastel frosted-glass material (no public-API equivalent exists for
UIVisualEffectView, so they are silently ignored on iOS).
<EdgeFadeView mode="blur" top={120} bottom={160} blurRadius={24} curve="gentle">
<ScrollView>{/* … */}</ScrollView>
</EdgeFadeView>Requires iOS 13+ / Android 12 (API 31)+. On older Android and on Web,
blurmode degrades gracefully to a transparentmaskfade.Blur needs opaque content. Blurring content with transparent gaps produces dark premultiplied-alpha fringes. Give the
EdgeFadeView(or its content) a solidbackgroundColor— the view composites that opaque backdrop before blurring.
Per-edge colors
In overlay mode each edge can have its own independent color:
<EdgeFadeView
top={{ color: '#1a1a2e' }}
bottom={{ color: '#16213e' }}
/>Curves
Presets
| Name | Shape |
| ------------ | ------------------------------------------------------------------ |
| 'smooth' | Cubic ease-out — default |
| 'smoother' | Smootherstep S-curve — eased at both ends, the least direct cutoff |
| 'sharp' | Quintic ease-out — aggressive |
| 'gentle' | Quadratic ease-out — soft |
| 'soft' | Sinusoidal — very gradual |
| 'linear' | Linear — constant rate |
cubicBezier
Standard CSS cubic-bezier() easing, sampled at 32 evenly-spaced positions:
// ease-in-out
<EdgeFadeView
bottom={{ curve: { type: 'cubicBezier', x1: 0.42, y1: 0, x2: 0.58, y2: 1 } }}
/>
// ease
<EdgeFadeView
bottom={{ curve: { type: 'cubicBezier', x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 } }}
/>stops
Explicit alpha array from inner edge (1.0) to outer edge (0.0):
// Concave — quick drop-off near the outer edge
<EdgeFadeView bottom={{ curve: { type: 'stops', values: [1, 0.9, 0.6, 0.2, 0] } }} />
// Plateau — holds at 50% before the final cut
<EdgeFadeView bottom={{ curve: { type: 'stops', values: [1, 0.5, 0.5, 0.5, 0] } }} />On Android API 33+, both
cubicBezierandstopsare rendered via an AGSLRuntimeShaderwith a 32-entry LUT uniform and per-pixel linear interpolation — no discrete banding regardless of curve shape.
Examples
Fading both ends of a vertical list
<EdgeFadeView top={40} bottom={80} style={{ flex: 1 }}>
<ScrollView nestedScrollEnabled showsVerticalScrollIndicator={false}>
{items.map((item) => <Row key={item.id} {...item} />)}
</ScrollView>
</EdgeFadeView>Horizontal strip with overlay fade
<View style={{ height: 48 }}>
<EdgeFadeView
mode="overlay"
left={32}
right={32}
color={backgroundColor}
curve="gentle"
style={StyleSheet.absoluteFill}
>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={false}
>
{tags.map((tag) => <Tag key={tag} label={tag} />)}
</ScrollView>
</EdgeFadeView>
</View>Rounded card with faded overlay
<EdgeFadeView bottom={60} radius={16} style={{ height: 200 }}>
<Image source={coverImage} style={StyleSheet.absoluteFill} />
</EdgeFadeView>All four edges simultaneously
<EdgeFadeView top={40} bottom={80} left={24} right={24} curve="gentle">
<ScrollView nestedScrollEnabled>
{/* horizontal + vertical content */}
</ScrollView>
</EdgeFadeView>Platform notes
| Platform | Implementation |
| ----------------- | --------------------------------------------------------------------------- |
| Android API 33+ | AGSL RuntimeShader — per-pixel curve evaluation, zero banding, dithered |
| Android API 29+ | BlendMode.DST_IN for mask compositing (legacy PorterDuffXfermode below) |
| Android API < 33 | LinearGradient with 64 discrete stops |
| Android API 31+ | blur mode — progressive stack of RenderEffect.createBlurEffect levels, clipped to the fade strips, masked by curve-aware plateau gradients (heavy levels at half resolution) |
| iOS | CALayer mask using CGGradient (kCGBlendModeDestinationIn) |
| iOS 13+ | blur mode — progressive stack of masked UIVisualEffectViews (pure Gaussian, no material tint), intensity via paused UIViewPropertyAnimator; public API only |
| Web | CSS mask-image + linear-gradient, mask-composite: intersect |
mode="blur"runs natively on iOS 13+ and Android 12+ (API 31); Web and older Android fall back tomask.
Android — nested scrolling
When EdgeFadeView wraps a ScrollView that is itself inside another ScrollView,
add nestedScrollEnabled to the inner view so touch events are not intercepted by
the parent:
<ScrollView>
<EdgeFadeView bottom={80} style={{ height: 300 }}>
<ScrollView nestedScrollEnabled> {/* required on Android */}
{/* … */}
</ScrollView>
</EdgeFadeView>
</ScrollView>Android — AGSL dithering
On API 33+, the AGSL shader applies subtle deterministic alpha dithering within the fade region. AGSL eliminates stop-based banding; dithering mitigates the residual 8-bit alpha/display quantization that can still appear on dark backgrounds or in compressed screenshots.
Animated fades (Reanimated)
For UI-thread animated fades, use AnimatedEdgeFadeView. It accepts the same
ergonomic props as EdgeFadeView and additionally a SharedValue<number> on
any size-like prop (top, bottom, left, right, start, end, radius).
Updates driven by a SharedValue stay on the UI thread — no bridge, no React
re-render. Static props (mode, curve, color, …) work as usual.
import Animated, {
Extrapolation,
interpolate,
useAnimatedScrollHandler,
useDerivedValue,
useSharedValue,
} from 'react-native-reanimated';
import { AnimatedEdgeFadeView } from 'react-native-edge-fade';
function FeedScreen() {
const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler((event) => {
scrollY.value = event.contentOffset.y;
});
// SharedValue derived from scroll position — fade depth grows as you scroll.
const topFade = useDerivedValue(() =>
interpolate(scrollY.value, [0, 80], [0, 60], Extrapolation.CLAMP)
);
return (
<AnimatedEdgeFadeView top={topFade} bottom={80} style={{ flex: 1 }}>
<Animated.ScrollView scrollEventThrottle={16} onScroll={onScroll}>
{/* content */}
</Animated.ScrollView>
</AnimatedEdgeFadeView>
);
}Reanimated is an optional peer dependency. Install
react-native-reanimatedand follow its Babel setup to useAnimatedEdgeFadeView. If Reanimated is missing the component throws a clear error on first render; the staticEdgeFadeViewworks without it.
Power-user escape hatch
The raw Fabric component is available via a dedicated subpath for cases that
need full control over the flat native props (fadeTop, fadeBottom, …,
fadeRadius):
import { NativeEdgeFadeView } from 'react-native-edge-fade/native';Most apps never need this — prefer AnimatedEdgeFadeView.
TypeScript
All types are exported from the package root:
import type {
EdgeFadeViewProps,
AnimatedEdgeFadeViewProps,
EdgeFadeCurve,
EdgeFadeMode,
EdgeConfig,
CubicBezierCurve,
StopsCurve,
} from 'react-native-edge-fade';Development
yarn # install dependencies
yarn typecheck # TypeScript check
yarn lint # ESLint + Prettier
yarn test # Jest unit tests
yarn prepare # build the library (outputs to lib/)License
MIT © Giulio Amato
