Skip to content

Instantly share code, notes, and snippets.

@kacperkapusciak
Last active July 9, 2025 07:05
Show Gist options
  • Save kacperkapusciak/263aa832514fde4baa07bda108183159 to your computer and use it in GitHub Desktop.
Save kacperkapusciak/263aa832514fde4baa07bda108183159 to your computer and use it in GitHub Desktop.
React Native Reanimated 4 view outline animation
import Feather from "@expo/vector-icons/Feather";
import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import MapView from "react-native-maps";
import Animated from "react-native-reanimated";
const colors = {
primary: "#3b82f6",
secondary: "#ef4444",
};
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export default function Index() {
const [pressed, setPressed] = useState(false);
const [isActive, setIsActive] = useState(false);
return (
<View style={styles.container}>
<Animated.View
style={[
styles.gradientEdge,
{
transitionProperty: "opacity",
transitionDuration: "0.3s",
transitionTimingFunction: "ease-in",
opacity: isActive ? 0.6 : 1,
},
]}
/>
<Animated.View
style={[
styles.sheet,
{
transitionProperty: "opacity",
transitionDuration: "0.3s",
transitionTimingFunction: "ease-in",
opacity: isActive ? 0.6 : 1,
},
]}
/>
<AnimatedPressable
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
onPress={() => setIsActive(!isActive)}
style={[
styles.button,
{
transform: [
{ scale: pressed ? 0.98 : 1 },
{ translateY: pressed ? 2 : 0 },
],
transitionProperty: "transform",
transitionDuration: "0.2s",
transitionTimingFunction: "ease-in-out",
},
]}
>
<Animated.View
style={[
styles.buttonBackground,
{
backgroundColor: isActive ? colors.secondary : colors.primary,
outlineColor: isActive ? "transparent" : colors.primary,
animationIterationCount: "infinite",
animationDuration: "16s",
animationName: {
"100%": {
transform: [{ rotate: "360deg" }],
},
},
animationTimingFunction: "linear",
transitionProperty: ["backgroundColor", "outlineColor"],
transitionDuration: "0.4s",
transitionTimingFunction: "ease-in-out",
},
]}
/>
<View style={styles.buttonInner}>
{isActive ? (
<Feather name="square" size={36} color="white" />
) : (
<Text style={styles.buttonText}>Run</Text>
)}
</View>
</AnimatedPressable>
<MapView
style={styles.map}
initialRegion={{
latitude: 50,
longitude: 20,
latitudeDelta: 0.06,
longitudeDelta: 0.06,
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
gradientEdge: {
experimental_backgroundImage:
"linear-gradient(0deg,rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);",
position: "absolute",
bottom: 150,
left: 0,
right: 0,
height: 64,
zIndex: 1,
},
sheet: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 150,
backgroundColor: "white",
zIndex: 1,
justifyContent: "center",
alignItems: "center",
},
buttonInner: {
position: "absolute",
width: 88,
height: 88,
justifyContent: "center",
alignItems: "center",
zIndex: 2,
},
map: {
width: "100%",
height: "100%",
},
button: {
zIndex: 2,
position: "absolute",
bottom: 35,
},
buttonBackground: {
width: 88,
height: 88,
borderRadius: 44,
justifyContent: "center",
alignItems: "center",
zIndex: 2,
outlineOffset: 3,
outlineWidth: 3,
outlineStyle: "dashed",
},
buttonText: {
color: "white",
fontWeight: "bold",
fontSize: 20,
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment