Fixing FlashCard flip animation rendering issues in React Native
In the K-PopRama Slang application, one of the features I developed was a FlashCard study screen. Users can tap a card to reveal its meaning and example sentences, then mark their answer as correct or wrong.
The basic interaction was straightforward: rotate the card around its vertical axis to reveal the back. However, while implementing the animation, I encountered several rendering issues. The card sometimes interfered with other UI components, and its appearance became distorted during rotation.
In this post, I will explain how I implemented the flip animation and the changes that helped me resolve these problems.
The FlashCard design
The study screen contains three main areas:
Study toggle: Switches between Daily Picks and Random Study.
FlashCard: Displays a Korean slang word on the front and its meaning and example on the back.
Study progress: Shows the user's progress through the current set of cards.
The front of the card displays the Korean word, its pronunciation, and a hint telling the user to tap to flip it. The back displays the definition and an example sentence. It also includes Correct and Wrong buttons so the user can record whether they remembered the word.
I used React Native's Animated API to create the rotation. The front rotates from 0 to 180 degrees, while the back rotates from 180 to 360 degrees. Both faces occupy the same space, so one face can be revealed as the other rotates away.
Implementing the rotation
I used an Animated.Value to represent the animation progress, from 0 to 1.
const rotateAnim = useRef(new Animated.Value(0)).current; const frontInterpolate = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'], }); const backInterpolate = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['180deg', '360deg'], });
When the user taps the front of the card, I animate the value to 1:
Animated.timing(rotateAnim, { toValue: 1, duration: 300, useNativeDriver: true, }).start();
When the user selects Correct or Wrong, the card returns to its initial state by animating the value back to 0.
I also used perspective to make the rotation look more like a three-dimensional card turning in space:
transform: [ { perspective: 1000 }, { rotateY: frontInterpolate }, ]
The order matters in this transform array: I placed perspective before rotateY. Without it, the rotation did not have the same visual depth.
However, getting the rotation itself to work was only part of the implementation. I still needed to handle how the two card faces interacted with the rest of the screen.
Fixing the rendering issue with zIndex
The first important issue involved the stacking order of the card and the surrounding UI.
The front and back faces were positioned on top of each other using absolute positioning:
card: { position: 'absolute', width: '100%', height: '100%', backfaceVisibility: 'hidden', }, front: { zIndex: 2, }, back: { zIndex: 1, },
The zIndex values determine which face appears in front of the other. I also used backfaceVisibility: 'hidden' so that the reverse side of a face would not remain visible when it rotated away.
Initially, I had set the stacking order of the front and back faces, but I had not explicitly set zIndex on the FlashCard's outer container.
That turned out to be an important omission. During rotation, the card could appear to interfere with nearby components, including the study toggle and progress indicator. The problem was particularly noticeable on iOS, where the screen could render incorrectly while the card was turning.
My fix was to assign a zIndex to the card's parent container:
container: { flex: 1, marginHorizontal: 16, marginTop: 16, marginBottom: 16, zIndex: 1, },
This made the stacking order of the FlashCard container explicit relative to neighboring components. Inside that container, the front and back faces could then use their own zIndex values to determine which face should appear on top.
This distinction was the key lesson for me: setting the stacking order of child views does not replace managing the stacking order of their parent container relative to neighboring components.
After adding the parent container's zIndex, the rotation no longer caused the same visible interference with the surrounding UI in my testing.
Preventing the hidden face from receiving touches
Another detail was making sure that the invisible side of the card did not interfere with touch interactions.
Even when the front face had rotated away, its view still existed. I used pointerEvents to control which face could receive touch events based on the current flip state:
// Front face pointerEvents={isFlipped ? 'none' : 'auto'} // Back face pointerEvents={isFlipped ? 'auto' : 'none'}
When the card is showing its front, the front face receives touches and the back face does not. After the card flips, those settings are reversed.
This prevents a visually hidden face from intercepting taps intended for the visible face. It is especially important on the back of my card, where users need to tap the Correct and Wrong buttons.
Handling Android shadows during rotation
I also noticed a separate visual problem on Android. The card used elevation to create a shadow, but the shadow could look distorted while the card was rotating.
The iOS shadow properties and Android's elevation do not behave identically. My iOS implementation used properties such as shadowColor, shadowOffset, shadowOpacity, and shadowRadius. On Android, I used elevation.
Rather than removing the shadow completely, I wanted to preserve it when the card was stationary and hide it while the rotation was in progress.
I used interpolation to animate the elevation along with the rotation:
const shadowElevation = rotateAnim.interpolate({ inputRange: [0, 0.01, 0.99, 1], outputRange: [4, 0, 0, 4], extrapolate: 'clamp', });
I then applied the interpolated value to both faces:
elevation: shadowElevation,
At the start and end of the animation, the card has an elevation of 4. During almost the entire rotation, the elevation is 0.
This was a practical workaround for the shadow artifact I encountered. It allowed the card to retain its shadow in its resting state without displaying the same distorted shadow throughout the animation.
Putting the pieces together
The final implementation combined several small adjustments rather than relying on the rotation animation alone:
rotateYanimates the front and back faces in opposite directions.perspectiveadds a three-dimensional appearance.backfaceVisibility: 'hidden'hides the reverse side of each face.Child and parent
zIndexvalues establish the intended stacking order.pointerEventsensures that only the visible face receives touches.Animated
elevationhides the Android shadow during rotation.
The card uses a 300-millisecond animation in both directions. After the user selects Correct or Wrong, the app reports the answer through the onNext callback and resets the card to its front face.
These details may seem minor individually, but together they made the interaction more reliable and visually consistent.
What I learned
This debugging experience taught me that a 3D-style animation is not just about rotating a view. It also depends on the stacking order of the view hierarchy, visibility of the reverse face, touch handling, and platform-specific rendering behavior.
The most important fix in my case was adding zIndex to the FlashCard's parent container. The separate Android shadow issue required a different solution: interpolating elevation so that the shadow disappeared during rotation and returned when the animation finished.
When I build animated UI components in the future, I will test them alongside neighboring components rather than checking only the animated element in isolation. I will also test both iOS and Android because a layout or animation that looks correct on one platform may behave differently on the other.
For K-PopRama Slang, these changes helped turn a basic rotating card into a more reliable study interaction. The experience was a useful reminder that small details in React Native's view hierarchy can make a significant difference to the final user experience.


Comments
Post a Comment