Blur Carousel
A horizontal carousel where item scale and fade as you scroll with a soft blur
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated @sbaiahmed1/react-native-blurCopy and paste the following code into your project.
component/molecules/blur-carousel
import { Dimensions, StyleSheet, View, ViewStyle } from "react-native";import React, { memo } from "react";import { BlurCarouselItemProps, BlurCarouselProps } from "./types";import { BlurView, type BlurViewProps } from "@sbaiahmed1/react-native-blur";import Animated, { interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, Extrapolation, useAnimatedProps,} from "react-native-reanimated";const AnimatedBlurView = Animated.createAnimatedComponent<BlurViewProps>(BlurView);const { width: SCREEN_WIDTH } = Dimensions.get("window");const ITEM_WIDTH = SCREEN_WIDTH * 0.75;const SPACING = 20;const SIDE_SPACING = (SCREEN_WIDTH - ITEM_WIDTH) / 2;const CarouselItem = <ItemT,>({ item, index, scrollX, renderItem, itemWidth = ITEM_WIDTH, spacing = SPACING,}: BlurCarouselItemProps<ItemT>): React.ReactNode & React.JSX.Element & React.ReactElement => { const animatedStyle = useAnimatedStyle< Required<Partial<Pick<ViewStyle, "transform" | "opacity">>> >(() => { const inputRange = [ (index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth, ]; const scale = interpolate( scrollX.value, inputRange, [0.85, 1, 0.85], Extrapolation.CLAMP, ); const opacity = interpolate( scrollX.value, inputRange, [0.5, 1, 0.5], Extrapolation.CLAMP, ); return { transform: [{ scale }], opacity, }; }); const animatedBlurProps = useAnimatedProps(() => { const inputRange = [ (index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth, ]; const blurIntensity = interpolate( scrollX.value, inputRange, [25, 0, 25], Extrapolation.CLAMP, ); return { blurAmount: blurIntensity, }; }); return ( <Animated.View style={[ styles.itemContainer, animatedStyle, { width: itemWidth, }, ]} > <View style={[ styles.itemContent, { width: itemWidth - spacing * 2, }, ]} > {renderItem({ item, index })} <AnimatedBlurView style={[StyleSheet.absoluteFillObject, styles.blurOverlay]} blurType="light" animatedProps={animatedBlurProps} reducedTransparencyFallbackColor="transparent" /> </View> </Animated.View> );};const BlurCarousel = <ItemT,>({ data, renderItem, horizontalSpacing = SIDE_SPACING, itemWidth = ITEM_WIDTH, spacing = SPACING,}: BlurCarouselProps<ItemT>) => { const scrollX = useSharedValue(0); const onScroll = useAnimatedScrollHandler({ onScroll: (event) => { scrollX.value = event.contentOffset.x; }, }); return ( <Animated.FlatList data={data} showsHorizontalScrollIndicator={false} onScroll={onScroll} scrollEventThrottle={16} keyExtractor={(_, index) => index.toString()} horizontal pagingEnabled snapToInterval={itemWidth} decelerationRate="fast" contentContainerStyle={{ paddingHorizontal: horizontalSpacing, }} style={{ flexGrow: 0 }} renderItem={({ item, index }) => ( <CarouselItem item={item} index={index} scrollX={scrollX} renderItem={renderItem} itemWidth={itemWidth} spacing={spacing} /> )} /> );};const styles = StyleSheet.create({ itemContainer: { justifyContent: "center", alignItems: "center", }, itemContent: { borderRadius: 20, overflow: "hidden", shadowColor: "#000", shadowOffset: { width: 0, height: 8, }, shadowOpacity: 0.2, shadowRadius: 12, elevation: 10, }, blurOverlay: { borderRadius: 20, },});export { BlurCarousel, BlurCarouselItemProps, BlurCarouselProps };Usage
import { View, Text, StyleSheet } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import { SymbolView } from "expo-symbols";import { BlurCarousel } from "@/components/molecules/blur-carousel";import { LinearGradient } from "expo-linear-gradient";export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), Coolvetica: require("@/assets/fonts/CoolveticaLt-Regular.ttf"), HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"), }); const DATA = [ { id: "1", title: "Design", subtitle: "Create something beautiful", description: "Transform ideas into stunning visuals with intuitive tools", icon: "paintbrush.fill", gradient: ["#ff375f", "#ff6b8a"], stats: { value: "2.4k", label: "Projects" }, }, { id: "2", title: "Develop", subtitle: "Build with precision", description: "Write clean code and ship features faster than ever", icon: "chevron.left.forwardslash.chevron.right", gradient: ["#5e5ce6", "#8b8bf5"], stats: { value: "18ms", label: "Response" }, }, { id: "3", title: "Launch", subtitle: "Ship to the world", description: "Deploy globally with confidence and reliability", icon: "paperplane.fill", gradient: ["#30d158", "#5de37a"], stats: { value: "99.9%", label: "Uptime" }, }, ]; return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <View style={styles.header}> <Text style={[ styles.title, fontLoaded && { fontFamily: "HelveticaNowDisplay" }, ]} > Workflow </Text> </View> <BlurCarousel data={DATA} renderItem={({ item }) => ( <View style={styles.card}> <LinearGradient colors={item.gradient as any} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={styles.cardGradient} /> <View style={styles.cardTop}> <View style={styles.cardIcon}> <SymbolView name={item.icon as any} size={28} tintColor="#fff" /> </View> <View style={styles.badge}> <SymbolView name="star.fill" size={10} tintColor="#fff" /> <Text style={[ styles.badgeText, fontLoaded && { fontFamily: "SfProRounded" }, ]} > Pro </Text> </View> </View> <View style={styles.cardMiddle}> <Text style={[ styles.cardTitle, fontLoaded && { fontFamily: "Coolvetica" }, ]} > {item.title} </Text> <Text style={[ styles.cardSubtitle, fontLoaded && { fontFamily: "SfProRounded" }, ]} > {item.subtitle} </Text> <Text style={[ styles.cardDescription, fontLoaded && { fontFamily: "SfProRounded" }, ]} > {item.description} </Text> </View> <View style={styles.cardBottom}> <View style={styles.statBox}> <Text style={[ styles.statValue, fontLoaded && { fontFamily: "HelveticaNowDisplay" }, ]} > {item.stats.value} </Text> <Text style={[ styles.statLabel, fontLoaded && { fontFamily: "SfProRounded" }, ]} > {item.stats.label} </Text> </View> <View style={styles.arrowButton}> <SymbolView name="arrow.right" size={18} tintColor="#fff" /> </View> </View> </View> )} /> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#0a0a0a", }, header: { paddingHorizontal: 20, paddingTop: 70, paddingBottom: 40, }, title: { fontSize: 42, fontWeight: "700", color: "#fff", }, card: { width: "100%", height: 380, borderRadius: 28, overflow: "hidden", padding: 24, justifyContent: "space-between", }, cardGradient: { ...StyleSheet.absoluteFillObject, }, cardTop: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", }, cardIcon: { width: 60, height: 60, borderRadius: 20, backgroundColor: "rgba(255,255,255,0.2)", justifyContent: "center", alignItems: "center", }, badge: { flexDirection: "row", alignItems: "center", gap: 4, backgroundColor: "rgba(0,0,0,0.2)", paddingHorizontal: 10, paddingVertical: 6, borderRadius: 12, }, badgeText: { fontSize: 12, fontWeight: "600", color: "#fff", }, cardMiddle: { gap: 8, }, cardTitle: { fontSize: 44, fontWeight: "600", color: "#fff", }, cardSubtitle: { fontSize: 18, fontWeight: "600", color: "rgba(255,255,255,0.9)", }, cardDescription: { fontSize: 14, color: "rgba(255,255,255,0.6)", lineHeight: 20, marginTop: 4, }, cardBottom: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-end", }, statBox: { gap: 2, }, statValue: { fontSize: 32, fontWeight: "700", color: "#fff", }, statLabel: { fontSize: 13, color: "rgba(255,255,255,0.6)", }, arrowButton: { width: 48, height: 48, borderRadius: 24, backgroundColor: "rgba(0,0,0,0.2)", justifyContent: "center", alignItems: "center", },});Props
BlurCarouselItemProps
React Native Reanimated
React Native Blur
