Animated Drop Down in React Native

Posted in react-native-animations on September 11, 2024 by Hemanta Sapkota ‐ 10 min read

Animated Drop Down in React Native

Creating engaging and interactive user interfaces is crucial for modern mobile applications. One popular UI pattern is the stacked dropdown, which elegantly displays a list of options with smooth animations. In this blog post, we’ll walk through building a customizable and animated stacked dropdown component in React Native using React Native Reanimated and Lucide Icons.

Note: This tutorial is based on the work by Enzo Manuel Mangano. The original code has been adapted with minor modifications, including the use of Tailwind CSS for React Native instead of a traditional stylesheet, and Lucide icons instead of expo vector icons.

1. Overview

Dropdown menus are a staple in modern mobile applications, offering a convenient way to navigate between different sections or select options. However, standard dropdowns can sometimes feel static or unengaging. By implementing a stacked dropdown animation, we add a layer of interactivity that not only enhances the visual appeal but also improves the overall user experience.

Animation Preview

What You’ll Learn

In this tutorial, we’ll cover:

  1. Creating the basic dropdown item component.
  2. Implementing the animation logic.
  3. Adding interactive elements.
  4. Finalizing and testing the component.

Let’s dive in!

2. Creating the Basic Dropdown Item Component

We’ll begin by creating a foundational component for our dropdown items. This component will represent each item in the dropdown list.

Imports

import Color from 'color';
import { View, useWindowDimensions, Text } from 'react-native';
import Animated, {
  useAnimatedStyle,
  withSpring,
  withTiming,
} from 'react-native-reanimated';
import { LucideArrowRight, LucideIcon } from 'lucide-react-native';
  • Color: Used for color manipulations.
  • React Native Components: View, Text, and useWindowDimensions for layout and responsive design.
  • React Native Reanimated: Core library for animations.
  • Lucide Icons: Vector icons for UI elements.

Type Definitions

First, define the types to describe the structure of each dropdown item and the props that the StackedDropdownListItem component will receive:

type StackedDropDownItemType = {
  label: string;
  IconComponent: LucideIcon;
};

type StackedDropdownListItemProps = StackedDropDownItemType & {
  index: number;
  dropdownItemsCount: number;
  isExpanded: Animated.SharedValue<boolean>;
};
  • StackedDropDownItemType: Defines the structure of each dropdown item with a label and an icon component.
  • StackedDropdownListItemProps: Extends the item type by adding properties like index, dropdownItemsCount, and isExpanded to manage the state and positioning.

Component Structure

const StackedDropdownListItem: React.FC<StackedDropdownListItemProps> = ({
  label,
  IconComponent,
  index,
  dropdownItemsCount,
  isExpanded,
}) => {
  // Component logic here
};

The component accepts props defined by StackedDropdownListItemProps. It uses these props to determine its position, scale, and color based on whether the dropdown is expanded or collapsed.

3. Animated Styles

Calculating Dimensions and Positions

In this section, we determine the dynamic dimensions and positions of each dropdown item to ensure a responsive and visually appealing layout. By utilizing the device’s window width, we calculate the appropriate width for each item. We also define constants for the height and margin of the dropdown items. These values help in calculating the total height of the dropdown when expanded and the positioning of each item in both collapsed and expanded states. Additionally, we establish scale factors and background colors that vary based on the item’s index, contributing to a layered and depth-enhanced appearance.

const { width: windowWidth } = useWindowDimensions();
const DropdownListItemHeight = 85;
const Margin = 10;

const fullDropdownHeight = dropdownItemsCount * (DropdownListItemHeight + Margin);

const collapsedTop = fullDropdownHeight / 2 - DropdownListItemHeight;
const expandedTop = (DropdownListItemHeight + Margin) * index;

const expandedScale = 1;
const collapsedScale = 1 - index * 0.08;

const expandedBackgroundColor = '#1B1B1B';
const collapsedBackgroundColor = Color(expandedBackgroundColor)
  .lighten(index * 0.25)
  .hex();
  • Dimensions: windowWidth ensures the component is responsive.
  • DropdownListItemHeight & Margin: Define the size and spacing of each dropdown item.
  • fullDropdownHeight: Total height when all items are expanded.
  • collapsedTop & expandedTop: Positions for collapsed and expanded states.
  • Scale Factors: Different scales for visual depth in collapsed state.
  • Background Colors: Dark color when expanded; lighter shades when collapsed.

Defining Animated Styles

Here, we leverage Reanimated to create smooth and interactive animations for our dropdown items. The useAnimatedStyle hook allows us to define animated properties such as backgroundColor, top position, and transform effects like scale and translateY. These animated styles respond to the isExpanded shared value, enabling the dropdown items to transition seamlessly between their expanded and collapsed states. By using animation functions like withTiming and withSpring, we ensure that the transitions are both fluid and natural, enhancing the overall user experience.

const rStyle = useAnimatedStyle(() => {
  return {
    backgroundColor: withTiming(
      isExpanded.value ? expandedBackgroundColor : collapsedBackgroundColor
    ),
    top: withSpring(isExpanded.value ? expandedTop : collapsedTop),
    transform: [
      {
        scale: withSpring(isExpanded.value ? expandedScale : collapsedScale),
      },
      {
        translateY: fullDropdownHeight / 3,
      },
    ],
  };
}, []);
  • backgroundColor: Transitions between expanded and collapsed colors using withTiming.
  • top: Animates the vertical position with withSpring for a bouncy effect.
  • scale: Adjusts the size for depth perception.
  • translateY: Shifts the item vertically for stacking effect.

Handling header icons

This section focuses on managing the behavior and appearance of the header item and its associated icons within the dropdown. We first identify the header item by checking if its index is zero. For the left icon, we control its opacity based on whether the dropdown is expanded or collapsed, ensuring that icons are visible only when appropriate. Additionally, we handle the rotation of the arrow icon to provide visual feedback on the dropdown’s state. When the dropdown is expanded, the arrow rotates to indicate the change, signaling to users that the dropdown can be interacted with further.

const isHeader = index === 0;

const rLeftIconOpacityStyle = useAnimatedStyle(() => {
  return {
    opacity: withTiming(isHeader ? 1 : isExpanded.value ? 1 : 0),
  };
}, [isHeader]);

const rHeaderArrowIconStyle = useAnimatedStyle(() => {
  return {
    transform: [
      {
        rotate: withTiming(isHeader && isExpanded.value ? '90deg' : '0deg'),
      },
    ],
  };
});
  • isHeader: Determines if the current item is the header (first item).
  • Left Icon Opacity: Header always shows the icon; others show based on expansion state.
  • Arrow Rotation: Rotates the arrow when the header is expanded.

Rendering the Component

In the final part of the component, we assemble all the previously defined styles and behaviors into the actual UI elements. The Animated.View serves as the container for each dropdown item, applying the animated styles for positioning and appearance. We set up touch event handlers to toggle the expansion state when the header is interacted with. Inside the container, we arrange the icon, label, and arrow icon using View components, ensuring they are properly aligned and styled. The use of absolute positioning and z-indexing guarantees that each item stacks correctly, maintaining the intended layered effect of the stacked dropdown.

return (
  <Animated.View
    onTouchEnd={() => {
      if (isHeader) isExpanded.value = !isExpanded.value;
    }}
    style={[
      {
        zIndex: dropdownItemsCount - index,
        position: 'absolute',
        width: windowWidth * 0.95,
        height: DropdownListItemHeight,
        borderRadius: 10,
      },
      rStyle,
    ]}
  >
    <View className="flex-1 justify-center items-center">
      <Animated.View
        style={[
          {
            left: 15,
          },
          rLeftIconOpacityStyle,
        ]}
        className="absolute w-11 aspect-square bg-black rounded-lg justify-center items-center"
      >
        <IconComponent color={'yellow'} />
      </Animated.View>
      <Text className="text-[#D4D4D4] text-2xl uppercase tracking-wide">
        {label}
      </Text>
      <Animated.View
        style={[
          rHeaderArrowIconStyle,
          {
            right: 15,
            backgroundColor: 'transparent',
          },
        ]}
        className="absolute justify-center items-center"
      >
        <LucideArrowRight size={25} color={'#D4D4D4'} />
      </Animated.View>
    </View>
  </Animated.View>
);
  • Animated.View: The container that holds the entire dropdown item, with animated styles applied.
  • onTouchEnd: Toggles the isExpanded state when the header is touched.
  • zIndex: Ensures proper stacking order based on the item’s index.
  • Inner View: Centers the content vertically and horizontally.
  • Left Icon: Positioned absolutely with opacity controlled by animation.
  • Label: Displays the item’s label in styled text.
  • Arrow Icon: Indicates expandability, rotates when expanded.

3. Finalizing and Testing the Component

After implementing the component, it’s best to ensure it works seamlessly across different scenarios.

Testing on Various Devices

  • Performance: Verify that animations run smoothly without lag on different devices, including lower-end smartphones.
  • Layout Consistency: Ensure the dropdown aligns correctly and maintains its design across various screen sizes and orientations.
  • Usability: Test the dropdown to confirm that interactions are intuitive and responsive. Users should effortlessly expand and collapse the menu.

Accessibility Enhancements

The accessibility enhancements include adding an accessibilityLabel and accessibilityHint to the TouchableOpacity component, providing clear descriptions for screen readers about the dropdown’s purpose and current state.

<TouchableOpacity
  accessibilityLabel={`${label} dropdown`}
  accessibilityHint={`${
    isExpanded.value ? 'Collapse' : 'Expand'
  } the dropdown`}
  // ...other props
>
  {/* ...rest of the component */}
</TouchableOpacity>

4. Full source code & Usage

Parent container source

import { useSharedValue } from 'react-native-reanimated';
import { StackedDropDownItemType, StackedDropdownListItem } from './StackedDropDownItem';

type DropdownProps = {
  header: StackedDropDownItemType;
  options: StackedDropDownItemType[];
};

const StackedDropdown: React.FC<DropdownProps> = ({ header, options }) => {
  const dropdownItems = [header, ...options];
  const isExpanded = useSharedValue(false);

  return (
    <>
      {dropdownItems.map((item, index) => {
        return (
          <StackedDropdownListItem
            key={index}
            index={index}
            {...item}
            isExpanded={isExpanded}
            dropdownItemsCount={dropdownItems.length}
          />
        );
      })}
    </>
  );
};

export { StackedDropdown };
  1. Dropdown Items: Define an array of items with labels and corresponding icons.
  2. Shared Value: isExpanded manages the expanded/collapsed state across all dropdown items.
  3. Rendering Items: Map through the dropdownItems array to render each StackedDropdownListItem, passing necessary props.

Animated Drop Down Item Source

import Color from 'color';
import { View, useWindowDimensions, Text } from 'react-native';
import Animated, {
  useAnimatedStyle,
  withSpring,
  withTiming,
} from 'react-native-reanimated';
import { LucideArrowRight, LucideIcon } from 'lucide-react-native';

type StackedDropDownItemType = {
  label: string;
  IconComponent: LucideIcon;
};

type StackedDropdownListItemProps = StackedDropDownItemType & {
  index: number;
  dropdownItemsCount: number;
  isExpanded: Animated.SharedValue<boolean>;
};

const StackedDropdownListItem: React.FC<StackedDropdownListItemProps> = ({
  label,
  IconComponent,
  index,
  dropdownItemsCount,
  isExpanded,
}) => {
  const { width: windowWidth } = useWindowDimensions();
  const DropdownListItemHeight = 85;
  const Margin = 10;

  const fullDropdownHeight =
    dropdownItemsCount * (DropdownListItemHeight + Margin);

  const collapsedTop = fullDropdownHeight / 2 - DropdownListItemHeight;
  const expandedTop = (DropdownListItemHeight + Margin) * index;

  const expandedScale = 1;
  const collapsedScale = 1 - index * 0.08;

  const expandedBackgroundColor = '#1B1B1B';
  const collapsedBackgroundColor = Color(expandedBackgroundColor)
    .lighten(index * 0.25)
    .hex();

  const rStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: withTiming(
        isExpanded.value ? expandedBackgroundColor : collapsedBackgroundColor
      ),
      top: withSpring(isExpanded.value ? expandedTop : collapsedTop),
      transform: [
        {
          scale: withSpring(isExpanded.value ? expandedScale : collapsedScale),
        },
        {
          translateY: fullDropdownHeight / 3,
        },
      ],
    };
  }, []);

  const isHeader = index === 0;

  const rLeftIconOpacityStyle = useAnimatedStyle(() => {
    return {
      opacity: withTiming(isHeader ? 1 : isExpanded.value ? 1 : 0),
    };
  }, [isHeader]);

  const rHeaderArrowIconStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          rotate: withTiming(isHeader && isExpanded.value ? '90deg' : '0deg'),
        },
      ],
    };
  });

  return (
    <Animated.View
      onTouchEnd={() => {
        if (isHeader) isExpanded.value = !isExpanded.value;
      }}
      style={[
        {
          zIndex: dropdownItemsCount - index,
          position: 'absolute',
          width: windowWidth * 0.95,
          height: DropdownListItemHeight,
          borderRadius: 10,
        },
        rStyle,
      ]}
    >
      <View className="flex-1 justify-center items-center">
        <Animated.View
          style={[
            {
              left: 15,
            },
            rLeftIconOpacityStyle,
          ]}
          className="absolute w-11 aspect-square bg-black rounded-lg justify-center items-center"
        >
          <IconComponent color={'yellow'} />
        </Animated.View>
        <Text className="text-[#D4D4D4] text-2xl uppercase tracking-wide">
          {label}
        </Text>
        <Animated.View
          style={[
            rHeaderArrowIconStyle,
            {
              right: 15,
              backgroundColor: 'transparent',
            },
          ]}
          className="absolute justify-center items-center"
        >
          <LucideArrowRight size={25} color={'#D4D4D4'} />
        </Animated.View>
      </View>
    </Animated.View>
  );
};

export { StackedDropdownListItem  };
export type { StackedDropDownItemType };

Storybook usage

import React from 'react';
import {ScrollView, View} from 'react-native';
import {
  LucideBarChart,
  LucideBook,
  LucideCalendar,
  LucideCamera,
  LucideEllipsis,
} from 'lucide-react-native';
import {StackedDropdown} from '../StackedDropDown/StackedDropdown';

// Default export that contains the component metadata
export default {
  title: 'Animations/Stacked Drop Down',
  component: StackedDropdown,
  parameters: {
    notes: '',
  },
};

const options = [
  {label: 'Charts', IconComponent: LucideBarChart},
  {label: 'Book', IconComponent: LucideBook},
  {label: 'Calendar', IconComponent: LucideCalendar},
  {label: 'Camera', IconComponent: LucideCamera},
];

const header = {
  label: 'Header',
  IconComponent: LucideEllipsis,
};

export const Component = {
  render: () => (
    <ScrollView contentContainerStyle={{flexGrow: 1, alignItems: 'center'}}>
        <StackedDropdown header={header} options={options} />
    </ScrollView>
  ),
};

5. Conclusion

In this blog post, we explored how to build an animated stacked dropdown component in React Native using React Native Reanimated and Lucide Icons. The component offers a sleek and interactive UI element that can enhance the user experience of your mobile applications. By leveraging animated styles and responsive design principles, you can create dynamic and visually appealing interfaces that stand out.

Feel free to customize the component further to fit your application’s needs, such as adding more interactive elements, adjusting animation parameters, or integrating it with your navigation flow. Happy coding!

A special thanks to Enzo Manuel Mangano for the original implementation that inspired this tutorial. If you have any questions or encounter issues, feel free to leave a comment below—I’m here to help!

comments powered by Disqus