DEV Community

Cover image for Interactive Resizable Split View in React Native
haider mukhtar
haider mukhtar

Posted on

Interactive Resizable Split View in React Native

In modern mobile UI development, responsive layouts help make an app feel vibrant and alive. Static screens can feel rigid, but interactive layouts that adapt dynamically to user input create a noticeable sense of control and polish. This guide walks you through building an interactive, resizable split-screen view in React Native using popular libraries such as React Native Reanimated and Gesture Handler.

✨ Why Micro-animations Matter:

Micro-animations are small, functional animations that make a big difference in user experience:

  • Provide Feedback: Instantly Confirm User Actions.
  • Guide the User: Direct attention and maintain spatial awareness.
  • Add Polish: Make the UI feel crafted and deliberate. These animations only truly shine when paired with a consistent, well-designed UI foundation. Even the best animations can’t elevate an unpolished interface.

🏗️ Setting Up the Layout:

The screen splits into two sections: a top and a bottom pane. Using React Native Reanimated’s sharedValue,The height of the top section changes dynamically as the user drags a divider. Shared values update on the UI thread smoothly, without triggering costly re-renders.

Screen split layout

const HEADER_HEIGHT = 60;
const MIN_SECTION_HEIGHT = 100;
const MAX_TOP_SECTION_HEIGHT = SCREEN_HEIGHT * 0.7;
const DEFAULT_TOP_SECTION_HEIGHT = SCREEN_HEIGHT * 0.45;

export default function Index() {
  const topSectionHeight = useSharedValue(DEFAULT_TOP_SECTION_HEIGHT);

  const topSectionAnimatedStyle = useAnimatedStyle(() => ({
    height: topSectionHeight.value,
  }));

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaView style={styles.container}>

        {/* Top Section - Resizable */}
        <Animated.View style={[styles.topSection, topSectionAnimatedStyle]}>
        </Animated.View>

        {/* Bottom Section - Auto-adjusting */}
        <View style={styles.bottomSection}>
        </View>

      </SafeAreaView>
    </GestureHandlerRootView>
  )
};
Enter fullscreen mode Exit fullscreen mode

✋ Draggable with Gesture Handler:

Using the powerful gesture system from React Native Gesture Handler:

  • onStart records the starting height of the top section.
  • onStart records the starting height of the top section.
  • onUpdate adjusts the height dynamically, clamping it within min and max bounds to keep the UI stable.
  • onEnd snaps the height to the closest preset point (minimum, default, or maximum height) to optimize usability and predictability.
const ANIMATION_CONFIG = { damping: 25, stiffness: 300, mass: 0.8 };
const VELOCITY_THRESHOLD = 800;

const startY = useSharedValue(0);
const isDragging = useSharedValue(false);

const panGesture = Gesture.Pan()
  .onStart(() => {
    startY.value = topSectionHeight.value;
    isDragging.value = true;
  })
  .onUpdate((event) => {
    const newHeight = startY.value + event.translationY;
    topSectionHeight.value = Math.max(
      MIN_SECTION_HEIGHT,
      Math.min(newHeight, MAX_TOP_SECTION_HEIGHT)
    );
  })
  .onEnd((event) => {
    isDragging.value = false;
    const velocity = event.velocityY;
    let targetHeight;

    if (Math.abs(velocity) > VELOCITY_THRESHOLD) {
      targetHeight =
        velocity > 0 ? DEFAULT_TOP_SECTION_HEIGHT : MAX_TOP_SECTION_HEIGHT;
    } else {
      const distances = [
        {
          h: MIN_SECTION_HEIGHT,
          d: Math.abs(topSectionHeight.value - MIN_SECTION_HEIGHT),
        },
        {
          h: DEFAULT_TOP_SECTION_HEIGHT,
          d: Math.abs(topSectionHeight.value - DEFAULT_TOP_SECTION_HEIGHT),
        },
        {
          h: MAX_TOP_SECTION_HEIGHT,
          d: Math.abs(topSectionHeight.value - MAX_TOP_SECTION_HEIGHT),
        },
      ];
      targetHeight = distances.sort((a, b) => a.d - b.d)[0].h;
    }

    topSectionHeight.value = withSpring(targetHeight, ANIMATION_CONFIG);
  });
Enter fullscreen mode Exit fullscreen mode

Subtle handle scaling during drag:

const dragHandleAnimatedStyle = useAnimatedStyle(() => ({
  transform: [
    {
      scale: withSpring(isDragging.value ? 1.2 : 1, ANIMATION_CONFIG),
    },
  ],
}));

const dragHandleContainerAnimatedStyle = useAnimatedStyle(() => ({
  top: topSectionHeight.value - DRAG_HANDLE_HEIGHT / 2,
}));

{/* Drag Handle - Positioned at boundary */}
<GestureDetector gesture={panGesture}>
  <Animated.View
    style={[
      styles.dragHandleContainer,
      dragHandleContainerAnimatedStyle,
    ]}
  >
    <Animated.View
      style={[styles.dragHandle, dragHandleAnimatedStyle]}
    />
  </Animated.View>
</GestureDetector>
Enter fullscreen mode Exit fullscreen mode

📂 Code & GitHub Repository:

Check out the full source code on GitHub for hands-on exploration:

Repo: Interactive-Resizable-Split-View

📄 Key Dependencies:

  • react-native-reanimated: For shared values and animations.
  • react-native-gesture-handler: For pan gesture handling.
  • react-native-safe-area-context: For safe area handling.

🎯 Conclusion:
A resizable split view with smooth, responsive animations enhances your app’s interactivity and polish. Utilizing shared values improves performance, while snap points create predictable, user-friendly behavior. Carefully crafted micro-animations add meaningful feedback, transforming a static UI into one that truly feels alive.

Polishing the core layout and motion together delivers a compelling mobile experience that users appreciate.

🔗 Haider Mukhtar on LinkedIn
🔗 Haider Mukhtar on Bento

Top comments (1)

Collapse
 
haider_mukhtar profile image
haider mukhtar