Flexi Button
A compact animated button that expands form an icon into text
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated expo-blur @expo/vector-iconsCopy and paste the following code into your project.
component/micro-interactions/flexi-button.tsx
import { StyleSheet, Text, Pressable, type LayoutChangeEvent, type ViewStyle, type PressableProps,} from "react-native";import React, { useState, useCallback } from "react";import Animated, { interpolate, Extrapolation, useSharedValue, useAnimatedStyle, withSpring, useAnimatedProps, type AnimatedProps,} from "react-native-reanimated";import { Ionicons } from "@expo/vector-icons";import { BlurView, type BlurViewProps } from "expo-blur";import type { FlexiButtonProps } from "./types";const AnimatedBlurView = Animated.createAnimatedComponent<BlurViewProps>(BlurView);const AnimatedPressable = Animated.createAnimatedComponent<PressableProps>(Pressable);const FlexiButton: React.FC<FlexiButtonProps> = ({ onPress, collapsedWidth = 40, expandedWidth = 120, text = "Clear All", icon = "notifications", onDimensionsChange, backgroundColor = "rgba(255, 255, 255, 0.1)",}: FlexiButtonProps) => { const progress = useSharedValue<number>(0); const [isExpanded, setIsExpanded] = useState<boolean>(false); const handleLayout = useCallback( (event: LayoutChangeEvent) => { const { width, height, x, y } = event.nativeEvent.layout; onDimensionsChange?.({ width, height, x, y }); }, [onDimensionsChange], ); const handlePress = () => { const newState = !isExpanded; setIsExpanded(newState); progress.value = withSpring<number>(newState ? 1 : 0); onPress?.(); }; const animatedContainerStyle = useAnimatedStyle<ViewStyle>(() => { const width = interpolate( progress.value, [0, 1], [collapsedWidth, expandedWidth], Extrapolation.CLAMP, ); return { width, }; }); const animatedBlurProps = useAnimatedProps<AnimatedProps<BlurViewProps>>( () => { const intensity = withSpring<number>( interpolate( progress.value, [0, 0.2, 0.3, 0.5, 1], [0, 10, 15, 25, 0], Extrapolation.CLAMP, ), ); return { // @ts-ignore intensity, }; }, ); const animatedIconStyle = useAnimatedStyle<ViewStyle>(() => { const scale = interpolate( progress.value, [0, 1], [1, 0.8], Extrapolation.CLAMP, ); const opacity = interpolate( progress.value, [0, 0.3], [1, 0], Extrapolation.CLAMP, ); return { transform: [{ scale }], opacity, }; }); const animatedTextStyle = useAnimatedStyle(() => { const opacity = interpolate( progress.value, [0.1, 1], [0, 1], Extrapolation.CLAMP, ); const scale = interpolate( progress.value, [0, 1], [0.8, 1], Extrapolation.CLAMP, ); return { opacity, transform: [ { scale, }, ], }; }); return ( <AnimatedPressable onPress={handlePress} onLayout={handleLayout} style={[ styles.container, animatedContainerStyle, { backgroundColor: backgroundColor, }, ]} > <Animated.View style={styles.content}> <Animated.View style={[styles.iconContainer, animatedIconStyle]}> {typeof icon === "function" ? ( icon() ) : ( <Ionicons name={icon as any} size={18} color="#fff" /> )} </Animated.View> <Animated.View style={[styles.textContainer, animatedTextStyle]}> <Text style={styles.text} numberOfLines={1}> {text} </Text> </Animated.View> </Animated.View> <AnimatedBlurView tint="dark" style={[StyleSheet.absoluteFillObject]} animatedProps={animatedBlurProps} /> </AnimatedPressable> );};export { FlexiButton };const styles = StyleSheet.create({ container: { borderRadius: 99, justifyContent: "center", alignItems: "center", overflow: "hidden", height: 40, }, content: { width: "100%", height: "100%", justifyContent: "center", alignItems: "center", }, iconContainer: { width: 18, height: 18, justifyContent: "center", alignItems: "center", }, textContainer: { position: "absolute", left: 0, right: 0, justifyContent: "center", alignItems: "center", }, text: { color: "#fff", fontWeight: "600", fontSize: 14, },});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 { CountdownTimer } from "@/components/micro-interactions/countdown";import { Ionicons } from "@expo/vector-icons";import { FlexiButton } from "@/components/micro-interactions/flexi-button";export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"), Coolvetica: require("@/assets/fonts/Coolvetica-Rg.otf"), }); const launchDate = new Date("2026-07-20T14:30:00"); return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <View style={styles.content}> <FlexiButton /> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#0a0a0a", }, content: { alignItems: "center", gap: 24, top: 100, }, iconBox: { width: 64, height: 64, borderRadius: 20, backgroundColor: "#1a1a1a", justifyContent: "center", alignItems: "center", marginBottom: 8, }, label: { fontSize: 14, color: "#555", textTransform: "uppercase", letterSpacing: 2, }, date: { fontSize: 15, color: "#333", marginTop: 8, },});Props
React Native Reanimated
Expo Vector Icons
Expo Blur
