/react-native-collapsible-tab-view

A cross-platform Collapsible Tab View component for React Native

Primary LanguageTypeScriptMIT LicenseMIT

react-native-collapsible-tab-view

Build Status Version MIT License runs with expo

Expo app

If you are looking for the integration with react-native-tab-view and/or react-navigation, you need to use the v2, we are currenlty on v3.

Collapsible Tab View for React Native, with Reanimated.

Credits

The react-native-tab-view example app was used as template for the demos.

Demo

Default Snap DiffClamp DiffClamp + Snap

Features

  • Animations and interactions on the UI thread
  • Highly customizable
  • Fully typed with TypeScript
  • Lazy support with fade-in animation
  • DiffClamp header
  • Interpolated header
  • Scroll snap (with interpolated header)
  • Animated snap (with diffClamp header)
  • Scrollable tabs, inspired by the react-native-tab-view tab bar
  • Support horizontal and vertical window

Installation

Open a Terminal in the project root and run:

yarn add react-native-collapsible-tab-view

Then, add Reanimated v2, follow the official installation guide.

Quick Start

import React from 'react'
import { View, StyleSheet, ListRenderItem } from 'react-native'
import {
  RefComponent,
  ContainerRef,
  createCollapsibleTabs,
  TabBarProps as TabProps,
} from 'react-native-collapsible-tab-view'
import { useAnimatedRef } from 'react-native-reanimated'

type TabNames = 'A' | 'B'
type HeaderProps = TabProps<TabNames>

const { useTabsContext, ...Tabs } = createCollapsibleTabs<TabNames>()

const HEADER_HEIGHT = 250

const Example: React.FC = () => {
  const containerRef = useAnimatedRef<ContainerRef>()
  const tabARef = useAnimatedRef<RefComponent>()
  const tabBRef = useAnimatedRef<RefComponent>()

  const [refMap] = React.useState({
    A: tabARef,
    B: tabBRef,
  })

  return (
    <Tabs.Container
      containerRef={containerRef}
      HeaderComponent={Header}
      headerHeight={HEADER_HEIGHT} // optional
      refMap={refMap}
    >
      <ScreenA />
      <ScreenB />
    </Tabs.Container>
  )
}

const ScreenB = () => {
  return (
    <Tabs.ScrollView name="B">
      <View style={[styles.box, styles.boxA]} />
      <View style={[styles.box, styles.boxB]} />
    </Tabs.ScrollView>
  )
}

const renderItem: ListRenderItem<number> = ({ index }) => {
  return (
    <View style={[styles.box, index % 2 === 0 ? styles.boxB : styles.boxA]} />
  )
}

const ScreenA = () => {
  return (
    <Tabs.FlatList
      name="A"
      data={[0, 1, 2, 3, 4]}
      renderItem={renderItem}
      keyExtractor={(v) => v + ''}
    />
  )
}

const Header: React.FC<HeaderProps> = () => {
  return <View style={styles.header} />
}

const styles = StyleSheet.create({
  box: {
    height: 250,
    width: '100%',
  },
  boxA: {
    backgroundColor: 'white',
  },
  boxB: {
    backgroundColor: '#D8D8D8',
  },
  header: {
    height: HEADER_HEIGHT,
    width: '100%',
    backgroundColor: '#2196f3',
  },
})

export default Example

Scroll on header

If you want to allow scrolling from the header:

  • If the HeaderComponent doesn't contain touchables set pointerEvents='none'

  • If HeaderComponent does contain touchables set pointerEvents='box-none' for them to work.

    Note: With this setting any child component that should not respond to touches (e.g. <Image />) needs to have pointerEvents set to 'none'. Otherwise it can become the target of a touch gesture on iOS devices and thereby preventing scrolling.

API reference

createCollapsibleTabs

Basic usage looks like this:

import { createCollapsibleTabs, TabBarProps } from 'react-native-collapsible-tab-view'

type MyTabs = 'tab0' | 'tab1'

const {
  Container,
  FlatList,
  ScrollView,
  useTabsContext
} = createCollapsibleTabs<MyTabs>()

// or
const { useTabsContext, ...Tabs } = createCollapsibleTabs<MyTabs>()

// use like this:
<Tabs.Container {...props} />
<Tabs.FlatList name='tab0' {...props} />
<Tabs.ScrollView name='tab1' {...props} />

// if you want to provide a cutom tab bar
// TabBarComponent prop of the Tabs.Container
const { useTabsContext, ...Tabs } = createCollapsibleTabs<
    MyTabs,
    TabBarProps<MyTabs> & MyPropsType
  >()

Tabs.Container

Basic usage looks like this:

import {
  RefComponent,
  ContainerRef,
  TabBarProps,
} from 'react-native-collapsible-tab-view'
import { useAnimatedRef } from 'react-native-reanimated'

type MyTabs = 'article' | 'contacts' | 'albums'

const MyHeader: React.FC<TabBarProps<MyTabs>> = (props) => {...}

const Example: React.FC<Props> = () => {
  const containerRef = useAnimatedRef<ContainerRef>()
  const tab0Ref = useAnimatedRef<RefComponent>()
  const tab1Ref = useAnimatedRef<RefComponent>()

  const [refMap] = React.useState({
    tab0: tab0Ref,
    tab1: tab1Ref,
  })

  return (
    <Tabs.Container
      containerRef={containerRef}
      HeaderComponent={MyHeader}
      headerHeight={HEADER_HEIGHT} // optional
      refMap={refMap}
    >
      { /* components returning Tabs.ScrollView || Tabs.FlatList */ }
    </Tabs.Container>
  )
}

Props

prop description default
containerRef Must be provided with useAnimatedRef<ContainerRef>() .
refMap Map of tab names and refs, must be the same order as the container children.
children Array of react elements. Each child should have a Tabs.ScrollView or Tabs.FlatList inside.
initialTabName? Initial tab name.
headerHeight? If you don't provide the header height, the pager will fade-in after getting it with onLayout.
tabBarHeight? - 48
snapEnabled? Enable snapping. Do scroll snapping if !diffClampEnabled, otherwise, do animated snapping. false
diffClampEnabled? Enable diff clamp. false
snapThreshold? Percentage of header height to make the snap effect. A number between 0 and 1. 0.5
HeaderComponent? React component to render above the tabbar.
TabBarComponent? React component to render above tab scenes. MaterialTabBar
headerContainerStyle? Styles applied to the header and tabbar container
containerStyle? Styles applied to the view container.
cancelTranslation? This will cancel the collapsible effect, and render a static tabbar / header. false
lazy? Mount the screen only when it's focused. It has a default fade in animation. false
cancelLazyFadeIn? Cancel the fade in animation if lazy={true} false
tabBarProps? Props passed to the TabBarComponent.
pagerProps? Props passed to the horizontal FlatList.

Tabs.ScrollView and Tabs.FlatList

Basic usage looks like this:

const ScrollViewScreen: React.FC<object> = () => {
  return (
    <Tabs.ScrollView name="myTabName0" {...scrollViewProps}>
      {children}
    </Tabs.ScrollView>
  )
}

const FlatListScreen: React.FC<object> = () => {
  return <Tabs.FlatList<ItemType> name="myTabName1" {...flatListProps} />
}

Props

prop description
name Name of the tab. Must be key of the refMap object provided to the Tabs.Container.
{...rest} ScrollView or FlatList props.

useTabsContext

A hook to access the context.

// iside your component
const { focusedTab, ...rest } = useTabsContext()

Props

prop description type
headerHeight number
tabBarHeight number
snapEnabled boolean
diffClampEnabled boolean
snapThreshold number
refMap Record<T, Ref>
scrollYCurrent Scroll position of current tab. Animated.SharedValue<number>
tabNames Tab names, same as the keys of refMap Animated.SharedValue<T[]>
index Current tab index. Animated.SharedValue<number>
scrollY Array of the scroll position of each tab. Animated.SharedValue<number[]>
scrollX Scroll x position of the tabs container. Animated.SharedValue<number>
oldAccScrollY Previous accumulted scrollY. Animated.SharedValue<number>
accScrollY Current accumulated scrollY Animated.SharedValue<number>
offset Offset. Animated.SharedValue<number>
isScrolling If is scrolling. Animated.SharedValue<boolean>
focusedTab Current focused tab name. Animated.SharedValue<T>
accDiffClamp Accumulated diffClamp value. Animated.SharedValue<number>
containerHeight? number

Contributing

While developing, you can run the example app to test your changes.

Please follow the angular commit message format.

Make sure your code passes TypeScript and ESLint. Run the following to verify:

yarn typescript
yarn lint

To fix formatting errors, run the following:

yarn lint -- --fix

Remember to add tests for your change if possible.