August 22, 2026
React native Glass Dom transition
AI Assistant screen animation , purely written with react native reanimated.
Kevin Roan3 min read
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;
