August 22, 2026

React native Glass Dom transition

AI Assistant screen animation , purely written with react native reanimated.

3 min read
Cover image for “React native Glass Dom transition”
reactreact-nativereanimatedskiadome-animationai

React native Glass Dom transition

https://x.com/AlbiaHossain/status/2089707264999624739 https://x.com/jmtrivedi/status/1986572630292631861/video/1

This post on Twitter ( x.com ) Inspired me to do it. I Still does not understand the full animation logic. but got some part of it and tried to do that with react native reanimation and expo-glass-component. Again for iOS users only :).

The code and comments are self explainatory. Please go through it.

import { useMemo ,useEffect,useState} from "react";
import {  Text, View ,Button, Dimensions} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { GlassView } from "expo-glass-effect";
import { useTheme } from "@/store/theme";

import createVoiceStyles, {
  SwipeButtonHeight,
} from "./styles/voice";
import Animated, { useAnimatedReaction, useAnimatedStyle, useSharedValue ,withTiming} from "react-native-reanimated";

const AnimatedGlassView = Animated.createAnimatedComponent(GlassView);

const halfScreenHeight = Dimensions.get("window").height / 2;

const offsetAnimationTrigger = ( Dimensions.get("window").height  / 2  ) / 2;

const VoiceScreen = () => {
  const [trigger,setTrigger] = useState(false)

  const buttonHeight = useSharedValue(SwipeButtonHeight)

  const insets = useSafeAreaInsets();
  const scrollPosition = useSharedValue(0);
  const animationPosition = useSharedValue(0);
  const isExpanded = useSharedValue(false);
  const dragFromExpanded = useSharedValue(false);

  const { colors: COLORS, isDark } = useTheme();
  const styles = useMemo(
    () => createVoiceStyles(COLORS, isDark),
    [COLORS, isDark],
  );

// react native gesture handler callback
  const panGesture = useMemo(
    () =>
      Gesture.Pan()
        .onBegin(() => {
          // checks wheather we are starting from the expeanded state or note. if yes, updates the state on ui thread
          dragFromExpanded.value = isExpanded.value;
        })
        .onUpdate((e) => {
          // checks if we are starting from expanded position or not.
          if (dragFromExpanded.value) {
            animationPosition.value =
              // brings back to bottom
              -halfScreenHeight + Math.max(e.translationY, 0);
            return;
          }
          // stopes when the button is already in expaned state
          if (isExpanded.value) return;
          // this is what animating the bottom postion
          scrollPosition.value = -e.translationY;
          animationPosition.value = e.translationY;
        })
        // triggers when the user stops dragging
        .onFinalize((e) => {
          if (dragFromExpanded.value) {
            // resets the state back
            dragFromExpanded.value = false;
            // when the drag positon exceeds the offset ( half the screen height )
            if (e.translationY >= offsetAnimationTrigger) {
              isExpanded.value = false;
              scrollPosition.value = 0;
              buttonHeight.value = withTiming(SwipeButtonHeight, { duration: 1000 });
              animationPosition.value = withTiming(0, { duration: 1000 });
            } else {
              animationPosition.value = withTiming(-halfScreenHeight, { duration: 100 });
            }
            return;
          }
          if (isExpanded.value) return;
          animationPosition.value = withTiming(0, { duration: 100 });
        }),
    [scrollPosition, animationPosition, isExpanded, dragFromExpanded, buttonHeight],
  );

  useEffect(() => {
    if (trigger) {
      buttonHeight.value =  withTiming(300, { duration: 2000 })
    } else {
      buttonHeight.value = withTiming(0, { duration: 2000 })
    }
  },[buttonHeight,trigger])

  // listens for the animationchange, when a certain offset is reached, then the glass animates by it's own.

  // cannot use useEffeect in here, since the value is inside the ui thread. 
  useAnimatedReaction(
    () => scrollPosition.value >= offsetAnimationTrigger,
    (reached, previous) => {
      if (reached && !previous) {
        isExpanded.value = true;
        buttonHeight.value = withTiming(80, { duration: 1000 });
        animationPosition.value = withTiming(-halfScreenHeight, { duration: 1000 });
      }
    },
  );


  const animatedGlassStyle = useAnimatedStyle(() => ({
    height: buttonHeight.value,
    width: buttonHeight.value,
    bottom: -buttonHeight.value / 2,
    transform: [{ translateY: animationPosition.value }],
  }))

  return (
    <View style={styles.root}>
      <GestureDetector gesture={panGesture}>
        <View style={{ flex: 1 }}>

      <View style={[styles.content, { paddingTop: insets.top }]}>
        <Text style={styles.headline}>
          The new era of Restaurant Assistant is here.
        </Text>
        <Button onPress={() => setTrigger(!trigger)} title="Trigger animation" />
      </View>

      <AnimatedGlassView
        style={[styles.swipeButton, animatedGlassStyle]}
        glassEffectStyle="regular"
        isInteractive
      />
        </View>
        </GestureDetector>
    </View>
  );
};

export default VoiceScreen;