Flexi Button

A compact animated button that expands form an icon into text

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated expo-blur @expo/vector-icons

Copy 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