Skip to main content

An accordion allows to show and hide a piece of content with a smooth animation. Commonly used in "FAQ" sections.

Loading...

The following implementation of an accordion relies on shared values. Leveraging shared values helps to prevent unnecessary re-renders. We define shared values using the useSharedValue hook.

  const height = useSharedValue(0);

The AccordionItem component encapsulates each item in the accordion. A height shared value manages the height of the item. The height dynamically adjusts based on the isExpanded prop, resulting in smooth expansion and collapse animations. The duration prop controls the duration of the animation.

Inside the AccordionItem, the children represent the content section. It can accommodate various types of content.

Accordion
function AccordionItem({
isExpanded,
children,
viewKey,
style,
duration = 500,
}) {
const height = useSharedValue(0);

const derivedHeight = useDerivedValue(() =>
withTiming(height.value * Number(isExpanded.value), {
duration,
})
);
const bodyStyle = useAnimatedStyle(() => ({
height: derivedHeight.value,
}));

return (
<Animated.View
key={`accordionItem_${viewKey}`}
style={[styles.animatedView, bodyStyle, style]}>
<View
onLayout={(e) => {
height.value = e.nativeEvent.layout.height;
}}
style={styles.wrapper}>
{children}
</View>
</Animated.View>
);
}

Bottom sheets are surfaces containing supplementary content, anchored to the bottom of the screen. They can provide users with quick access to contextual information, actions, or settings without interrupting their current workflow.

Looking for a ready-to-use solution? We recommend @gorhom/bottom-sheet.

Loading...

The BottomSheet component accepts props such as isOpen - a shared value indicating whether the bottom sheet is open or closed, toggleSheet - a function to toggle the visibility of the bottom sheet, and an optional duration for animation.

Bottom Sheet

function BottomSheet({ isOpen, toggleSheet, duration = 500, children }) {
const { colorScheme } = useColorScheme();
const height = useSharedValue(0);
const progress = useDerivedValue(() =>
withTiming(isOpen.value ? 0 : 1, { duration })
);

const sheetStyle = useAnimatedStyle(() => ({
transform: [{ translateY: progress.value * 2 * height.value }],
}));

const backgroundColorSheetStyle = {
backgroundColor: colorScheme === 'light' ? '#f8f9ff' : '#272B3C',
};

const backdropStyle = useAnimatedStyle(() => ({
opacity: 1 - progress.value,
zIndex: isOpen.value
? 1
: withDelay(duration, withTiming(-1, { duration: 0 })),
}));

return (
<>
<Animated.View style={[sheetStyles.backdrop, backdropStyle]}>
<TouchableOpacity style={styles.flex} onPress={toggleSheet} />
</Animated.View>
<Animated.View
onLayout={(e) => {
height.value = e.nativeEvent.layout.height;
}}

The height shared value is used to track the height of the bottom sheet, while the progress derived value interpolates between 0 and 1 based on the state of isOpen, controlling the animation of the bottom sheet.

function BottomSheet({ isOpen, toggleSheet, duration = 500, children }) {
const { colorScheme } = useColorScheme();
const height = useSharedValue(0);
const progress = useDerivedValue(() =>

The useAnimatedStyle hook helps in creating animated styles based on shared values. These styles are then applied to BottomSheet to make it visually dynamic by adding backdrop and translating bottom sheet to the top.

  );

const sheetStyle = useAnimatedStyle(() => ({
transform: [{ translateY: progress.value * 2 * height.value }],
}));

const backgroundColorSheetStyle = {
backgroundColor: colorScheme === 'light' ? '#f8f9ff' : '#272B3C',
};

Flip card allows you to display different content depending on whether the card is flipped or not. It can be especially useful when you do not want to display some data immediately after entering the screen (e.g. secure data) and only after fulfilling a certain condition or performing an action.

Loading...

For storing information about whether the card is flipped or not we use shared value with the useSharedValue hook. Using shared values helps to prevent unnecessary re-renders.

  const isFlipped = useSharedValue(false);

This allows us to interpolate values between 0-180 and 180-360 degrees, depending on whether the card is flipped or not. In addition, we use withTiming util which makes our animation smooth.

    const spinValue = interpolate(Number(isFlipped.value), [0, 1], [0, 180]);
const rotateValue = withTiming(`${spinValue}deg`, { duration });

The FlipCard component accepts several props: duration allows you to change the duration of the animation, setting direction to the value x allows you to change the direction of our animation, RegularContent and FlippedContent give ability to display different content for flipped and non flipped variants.

Flip Card
const FlipCard = ({
isFlipped,
cardStyle,
direction = 'y',
duration = 500,
RegularContent,
FlippedContent,
}) => {
const isDirectionX = direction === 'x';

const regularCardAnimatedStyle = useAnimatedStyle(() => {
const spinValue = interpolate(Number(isFlipped.value), [0, 1], [0, 180]);
const rotateValue = withTiming(`${spinValue}deg`, { duration });

return {
transform: [
isDirectionX ? { rotateX: rotateValue } : { rotateY: rotateValue },
],
};
});

const flippedCardAnimatedStyle = useAnimatedStyle(() => {
const spinValue = interpolate(Number(isFlipped.value), [0, 1], [180, 360]);
const rotateValue = withTiming(`${spinValue}deg`, { duration });

return {
transform: [
isDirectionX ? { rotateX: rotateValue } : { rotateY: rotateValue },
],
};
});

return (
<View>
<Animated.View
style={[
flipCardStyles.regularCard,
cardStyle,
regularCardAnimatedStyle,
]}>
{RegularContent}
</Animated.View>
<Animated.View
style={[
flipCardStyles.flippedCard,
cardStyle,
flippedCardAnimatedStyle,
]}>
{FlippedContent}
</Animated.View>
</View>
);
};

Floating Action Button provides user with easy-accessible panel with primary or most common actions on the screen.

Loading...

We use shared values to monitor if the button is expanded. The useSharedValue hook helps prevent unnecessary re-renders during state changes.

  const isExpanded = useSharedValue(false);

const handlePress = () => {
isExpanded.value = !isExpanded.value;

The state is toggled when the main Actions button is pressed, which triggers animations for other secondary buttons.

It also relies on animatable values. Leveraging animatable values of rotation and position enables smooth transition between the two states.

Floating Action Button
  const plusIconStyle = useAnimatedStyle(() => {
const moveValue = interpolate(Number(isExpanded.value), [0, 1], [0, 2]);
const translateValue = withTiming(moveValue);
const rotateValue = isExpanded.value ? '45deg' : '0deg';

return {
transform: [
{ translateX: translateValue },
{ rotate: withTiming(rotateValue) },
],
};
});

The FloatingActionButton is a reusable component that manages button styles, content and animations. For this we use props: buttonLetter, index and isExpanded.

const FloatingActionButton = ({ isExpanded, index, buttonLetter }) => {
const animatedStyles = useAnimatedStyle(() => {
const moveValue = isExpanded.value ? OFFSET * index : 0;
const translateValue = withSpring(-moveValue, SPRING_CONFIG);
const delay = index * 100;

const scaleValue = isExpanded.value ? 1 : 0;

return {
transform: [
{ translateY: translateValue },
{
scale: withDelay(delay, withTiming(scaleValue)),
},
],
};
});

return (
<AnimatedPressable style={[animatedStyles, styles.shadow, styles.button]}>
<Animated.Text style={styles.content}>{buttonLetter}</Animated.Text>
</AnimatedPressable>
);
};

We define the animated styles for the buttons within the FloatingActionButton component, passing the necessary values as props. The delay in their appearance on the screen is calculated based on the button's index. Buttons with a higher index will appear later and be positioned higher in the "column" of buttons.

Floating Action Button
  const animatedStyles = useAnimatedStyle(() => {
const moveValue = isExpanded.value ? OFFSET * index : 0;
const translateValue = withSpring(-moveValue, SPRING_CONFIG);
const delay = index * 100;

const scaleValue = isExpanded.value ? 1 : 0;

return {
transform: [
{ translateY: translateValue },
{
scale: withDelay(delay, withTiming(scaleValue)),
},
],
};
});

return (
<AnimatedPressable style={[animatedStyles, styles.shadow, styles.button]}>
<Animated.Text style={styles.content}>{buttonLetter}</Animated.Text>
</AnimatedPressable>
);
};

A marquee is an element used to display scrolling content horizontally within a confined space. It's commonly seen in applications to information such as news tickers, advertisements, or any content that needs continuous display within a limited area.

Looking for a ready-to-use solution? We recommend @animatereactnative/marquee.

Loading...

Now, let's understand how this example works:

The MeasureElement component measures the width of its children and passes this information to its parent component, Marquee.

Marquee
const MeasureElement = ({ onLayout, children }) => (
<Animated.ScrollView
horizontal
style={marqueeStyles.hidden}
pointerEvents="box-none">
<View onLayout={(ev) => onLayout(ev.nativeEvent.layout.width)}>
{children}
</View>
</Animated.ScrollView>
);

We use the useFrameCallback hook to execute the animation logic on each frame.

  useFrameCallback((i) => {
// prettier-ignore
offset.value += (coeff.value * ((i.timeSincePreviousFrame ?? 1) * childrenWidth)) / duration;
offset.value = offset.value % childrenWidth;
}, true);

It is located inside ChildrenScroller component that manages the scrolling animation by updating the offset value. It determines the horizontal translation of the child components, creates clones of the children and animates them horizontally based on the specified duration.

Marquee
const ChildrenScroller = ({
duration,
childrenWidth,
parentWidth,
reverse,
children,
}) => {
const offset = useSharedValue(0);
const coeff = useSharedValue(reverse ? 1 : -1);

React.useEffect(() => {
coeff.value = reverse ? 1 : -1;
}, [reverse]);

useFrameCallback((i) => {
// prettier-ignore
offset.value += (coeff.value * ((i.timeSincePreviousFrame ?? 1) * childrenWidth)) / duration;
offset.value = offset.value % childrenWidth;
}, true);

const count = Math.round(parentWidth / childrenWidth) + 2;
const renderChild = (index) => (
<TranslatedElement
key={`clone-${index}`}
index={index}
offset={offset}
childrenWidth={childrenWidth}>
{children}
</TranslatedElement>
);

return <Cloner count={count} renderChild={renderChild} />;
};

The Marquee component serves as the main orchestrator of the marquee effect. It calculates necessary dimensions, renders child components within a container, and coordinates the animation by utilizing the ChildrenScroller component.

Marquee
  const [parentWidth, setParentWidth] = React.useState(0);
const [childrenWidth, setChildrenWidth] = React.useState(0);

return (
<View
style={style}
onLayout={(ev) => {
setParentWidth(ev.nativeEvent.layout.width);
}}
pointerEvents="box-none">
<View style={marqueeStyles.row} pointerEvents="box-none">
<MeasureElement onLayout={setChildrenWidth}>{children}</MeasureElement>

{childrenWidth > 0 && parentWidth > 0 && (
<ChildrenScroller
duration={duration}
parentWidth={parentWidth}
childrenWidth={childrenWidth}
reverse={reverse}>
{children}
</ChildrenScroller>
)}
</View>
</View>
);
};

Section lists allow you to organize long lists of content by dividing them with headings.

Loading...

The primary component, SectionList, acts as the main orchestrator of the entire Section List interface. It coordinates the rendering of the table of contents and individual content sections.

Section List
            index={index}
sectionCardsRef={sectionCardsRef}
/>
)}
data={data}
estimatedItemSize={52}
ref={tableOfContentsRef}
/>
</View>
);
};

const SectionList = () => {
const selectedItem = useSharedValue('');
const visibleIndex = useSharedValue(0);
const sectionNames = SECTIONS.map((s) => s.name);
const sectionCardsRef = useRef(null);
const tableOfContentsRef = useRef(null);

return (
<SafeAreaView style={sectionListStyles.cardsContainer}>
<TableOfContents
data={sectionNames}
visibleIndex={visibleIndex}
sectionCardsRef={sectionCardsRef}

Within SectionList, there are two key components: TableOfContents and SectionCards.

TableOfContents is responsible for rendering the list of section names as a table of contents. It receives props such as data, visibleIndex, sectionCardsRef, and tableOfContentsRef to manage navigation and synchronization between the table of contents and section content.

Section List
      <Animated.Text
style={[
style,
sectionListStyles.tableOfContentsElement,
tableOfContentsElementTextStyle,
]}>
{item}
</Animated.Text>
</Pressable>
);
};

const TableOfContents = ({
data,
visibleIndex,
sectionCardsRef,
tableOfContentsRef,
}) => {
return (
<View style={sectionListStyles.tableOfContents}>
<FlashList
horizontal
showsHorizontalScrollIndicator={false}
renderItem={({ item, index }) => (
<TableOfContentsElement
item={item}

SectionCards, on the other hand, manages the rendering of individual sections and their corresponding content. It receives props: sections, visibleIndex, sectionCardsRef, and tableOfContentsRef to render the content sections and handle scrolling interactions.

Section List
    marginHorizontal: 4,
margin: 8,
overflow: 'hidden',
},
tableOfContents: {
top: 0,
},
});

const SectionCards = ({
sections,
visibleIndex,
sectionCardsRef,
tableOfContentsRef,
}) => {
const { colorScheme } = useColorScheme();
const heights = sections.map((_) => SECTION_HEIGHT);

const getOffsetStarts = () =>
heights.map((v, i) => heights.slice(0, i).reduce((x, acc) => x + acc, 0));

const onScroll = (event) => {
const offset = event.nativeEvent?.contentOffset?.y;

if (offset !== undefined) {
const distancesFromTop = getOffsetStarts().map((v) =>
Math.abs(v - offset)
);
const newIndex = distancesFromTop.indexOf(
Math.min.apply(null, distancesFromTop)
);
if (visibleIndex.value !== newIndex) {
tableOfContentsRef.current?.scrollToIndex({
index: newIndex,
animated: true,
});
}
visibleIndex.value = newIndex;
}
};

const sectionNameStyle = useAnimatedStyle(() => ({
color: colorScheme === 'light' ? '#001a72' : '#f8f9ff',
}));

const renderItem = ({ item }) => {
return (
<View>
<Animated.Text style={[sectionCardStyles.header, sectionNameStyle]}>
{item.name}
</Animated.Text>
<SectionCardsElement>
<Text style={sectionCardStyles.content}>{item.content}</Text>
</SectionCardsElement>
</View>
);
};

return (

The onScroll in SectionCards calculates the offset as the user scrolls through the content and determines which section is currently most visible on the screen. It is done by comparing the distance of each section from the top of the screen - it identifies the section closest to the viewport's top edge.

  },
});

const SectionCards = ({
sections,
visibleIndex,
sectionCardsRef,
tableOfContentsRef,
}) => {
const { colorScheme } = useColorScheme();
const heights = sections.map((_) => SECTION_HEIGHT);

const getOffsetStarts = () =>
heights.map((v, i) => heights.slice(0, i).reduce((x, acc) => x + acc, 0));

const onScroll = (event) => {
const offset = event.nativeEvent?.contentOffset?.y;

if (offset !== undefined) {
const distancesFromTop = getOffsetStarts().map((v) =>
Math.abs(v - offset)
);
const newIndex = distancesFromTop.indexOf(
Math.min.apply(null, distancesFromTop)

We use the useSharedValue hook to create mutable shared values across different components. For instance, selectedItem and visibleIndex are shared values used to manage the currently selected section and its visibility index.

            sectionCardsRef={sectionCardsRef}
/>

Additionally, we use useAnimatedStyle hook to define animated styles based on the shared values. Then, we apply these animated styles to components to create dynamic visual effects, such as changing font weights and adding bottom borders.

const useSelectedStyle = (selectedItem, item) =>
useAnimatedStyle(() => ({
fontWeight: selectedItem.value === item ? '600' : '400',
borderBottomWidth: selectedItem.value === item ? 1 : 0,

To enable interaction with the FlashList component - such as scrolling to specific sections, the code utilizes variables created using useRef such as sectionCardsRef and tableContentsRef

        data={data}
estimatedItemSize={52}

Here, the debounce function throttles the invocations of onScroll event handler which improves the performance.


function debounce(func, timeout = 100) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, timeout);
};

Slider allows users to adjust a value or control a setting by sliding a handle along a track. It is commonly used to adjust settings such as volume, brightness, or in this case, the width of a box.

Loading...

We use the useSharedValue hook to store the offset of the slider handle, allowing for smooth animation during sliding.

  const offset = useSharedValue(0);

This example is done using Pan gesture from react-native-gesture-handler library. It adjusts the handle's position and width of the box accordingly to the current offset. The offset is a shared value and is updated during the onChange event of the pan gesture.

Slider
  const pan = Gesture.Pan().onChange((event) => {
offset.value =
Math.abs(offset.value) <= MAX_VALUE
? offset.value + event.changeX <= 0
? 0
: offset.value + event.changeX >= MAX_VALUE
? MAX_VALUE
: offset.value + event.changeX
: offset.value;

const newWidth = INITIAL_BOX_SIZE + offset.value;
boxWidth.value = newWidth;
});

The useAnimatedStyle hook is used to create animated styles for both the box and the slider handle. This ensures that changes to the offset value result in smooth animations for both components.

Slider
  const boxStyle = useAnimatedStyle(() => {
return {
width: INITIAL_BOX_SIZE + offset.value,
};
});

const sliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});

Leveraging animated props allows us to run them on the UI thread instead of the JS thread. To prevent unnecessary re-renders when the text displaying the current width of the box changes, we used the useAnimatedProps hook.

Additionally, we opted for TextInput instead of Text because TextInput has a text property that can be animated, whereas Text only has children.

This approach also enabled us to animate TextInput using shared values.

Slider
  const animatedProps = useAnimatedProps(() => {
return {
text: `Box width: ${Math.round(boxWidth.value)}`,
defaultValue: `Box width: ${boxWidth.value}`,
};
});

A switch element is a user interface component that allows users to toggle between two or more states. It is commonly used to turn on/off a setting, enable/disable a feature, or select between options.

Loading...

The following implementation of a switch relies on animatable values. Leveraging animatable values of color and position enables smooth transition between the two states.

Switch
  const trackAnimatedStyle = useAnimatedStyle(() => {
const color = interpolateColor(
value.value,
[0, 1],
[trackColors.off, trackColors.on]
);
const colorValue = withTiming(color, { duration });

return {
backgroundColor: colorValue,
borderRadius: height.value / 2,
};
});

const thumbAnimatedStyle = useAnimatedStyle(() => {
const moveValue = interpolate(
Number(value.value),
[0, 1],
[0, width.value - height.value]
);
const translateValue = withTiming(moveValue, { duration });

return {
transform: [{ translateX: translateValue }],
borderRadius: height.value / 2,
};
});

We use the useSharedValue hook to store the dimensions of the element, which allows for precise calculation of position changes during the animation. The hook is there to prevent unnecessary re-renders.

  const height = useSharedValue(0);
const width = useSharedValue(0);

The values are updated during the onLayout event of the element.

      <Animated.View
onLayout={(e) => {
height.value = e.nativeEvent.layout.height;
width.value = e.nativeEvent.layout.width;
}}
style={[switchStyles.track, style, trackAnimatedStyle]}>

The Switch component can represent any boolean value passed as a prop. The state dynamically adjusts based on the value prop resulting in smooth transition animations. It enables passing any function using the onPress prop. The duration prop controls the duration of the animation. The style and trackColors props enable personalization.

Switch
const Switch = ({
value,
onPress,
style,
duration = 400,
trackColors = { on: '#82cab2', off: '#fa7f7c' },
}) => {
const height = useSharedValue(0);
const width = useSharedValue(0);

const trackAnimatedStyle = useAnimatedStyle(() => {
const color = interpolateColor(
value.value,
[0, 1],
[trackColors.off, trackColors.on]
);
const colorValue = withTiming(color, { duration });

return {
backgroundColor: colorValue,
borderRadius: height.value / 2,
};
});

const thumbAnimatedStyle = useAnimatedStyle(() => {
const moveValue = interpolate(
Number(value.value),
[0, 1],
[0, width.value - height.value]
);
const translateValue = withTiming(moveValue, { duration });

return {
transform: [{ translateX: translateValue }],
borderRadius: height.value / 2,
};
});

return (
<Pressable onPress={onPress}>
<Animated.View
onLayout={(e) => {
height.value = e.nativeEvent.layout.height;
width.value = e.nativeEvent.layout.width;
}}
style={[switchStyles.track, style, trackAnimatedStyle]}>
<Animated.View
style={[switchStyles.thumb, thumbAnimatedStyle]}></Animated.View>
</Animated.View>
</Pressable>
);
};