How To Add A Sticky Footer To The React Native Modal
Tags: reactnative
Published: 2021-07-31 10:13:36

---
title: How To Add A Sticky Footer To The React Native Modal
published: true
date: 2021-07-31 00:00:00 UTC
tags: reactnative
description: Complete with a linear gradient and styling alignments
canonical_url: https://bionicjulia.com/blog/add-sticky-footer-to-react-native-modal
---

I recently had to migrate the modals in our app across to the [React Native Modal](https://reactnative.dev/docs/modal) component. We were previously using an older library that is not longer maintained, and thought it'd be safer to go with RN's Modal, as it's a core component and does most of what we needed our modals to do out of the box.

In order to keep styling and behaviour consistent across the app, we created a custom `Modal` to encapsulate the style and behaviour we wanted.

```jsx
import { SafeLayout } from '@covid/components'
import * as React from 'react'
import { Modal as RNModal, SafeAreaView, ScrollView, StyleSheet } from 'react-native'

interface IProps {
  children: React.ReactNode;
  onRequestClose: () => void;
  visible: boolean;
}

export default function Modal(props: IProps) {
  return (
    <RNModal transparent animationType="fade" onRequestClose={props.onRequestClose} visible={props.visible}>
      <SafeAreaView style={styles.modal}>
        <ScrollView
          alwaysBounceVertical={false}
          contentContainerStyle={styles.contentContainer}
          showsVerticalScrollIndicator={false}
          style={styles.scrollView}
        >
          {props.children}
        </ScrollView>
      </SafeAreaView>
    </RNModal>
  )
}
```

As you can see, the props needed are really just the `onRequestClose` callback, and the boolean to make the modal `visible`. We set some defaults in the form of styles, disabling vertical bounce and not showing the vertical scroll indicator.

This was a simple implementation of a modal, and worked well for our needs...until last week when it didn't. Up until now, information we stuck in a modal tended to be short and snappy, with no need for any scrolling, since it would all fit within a single screen height. That changed last week when we had a lengthy bit of content that needed to be made available to the user, but wasn't _really_ necessary for them to read, if they didn't want / need to.

The solution we thought would work best, was in effect a modal that would pop up, with a scrollable area where the lengthy content would be displayed, and a sticky button at the bottom which would close the modal `onPress`.

Our current `Modal` component didn't allow for a sticky button - as you can see, all content for the `Modal` would be passed in via `props.children` which makes no distinction for sticky footers. What we therefore needed to do was allow the passing of a footer component into our `Modal`, in addition to having a fade off effect between the current content, and the top of the sticky footer. We felt this was needed to indicate to the user that there's more content below to read, if needed. We decided to use the [React Native Linear Gradient](https://github.com/react-native-linear-gradient/react-native-linear-gradient) package for this.

One issue that cropped up was that alignments for the vertical scrollbar, the edge of our modal and the linear gradient didn't match up well. There was a bit of fiddling around with the styling that needed to be done, to ensure it all looked good on iOS. The code below is where we ended up with our `Modal`.

```jsx
import * as React from 'react'
import { Modal as RNModal, SafeAreaView, ScrollView, StyleSheet, View } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'

interface IProps {
  children: React.ReactNode;
  footerChildren?: React.ReactNode;
  onRequestClose: () => void;
  visible: boolean;
  showVerticalScrollIndicator?: boolean;
}

const BORDER_RADIUS = 16
const CONTENT_SPACING = 20
const SCROLL_INDICATOR_OFFSET = BORDER_RADIUS / 4 + 2

// To ensure the scroll indicator aligns to the edge on iOS.
const INSETS = {
  bottom: -3,
  left: 0,
  right: -3,
  top: -3,
}

const COLORS = ['#ffffff00', 'white']

export default function Modal(props: IProps) {
  return (
    <RNModal transparent animationType="fade" onRequestClose={props.onRequestClose} visible={props.visible}>
      <SafeAreaView style={styles.safeLayout}>
        <View style={styles.view}>
          <View style={styles.view2}>
            <ScrollView
              alwaysBounceVertical={false}
              contentContainerStyle={styles.contentContainer}
              scrollIndicatorInsets={INSETS}
              showsVerticalScrollIndicator={props.showVerticalScrollIndicator || false}
              style={styles.scrollView}
            >
              {props.children}
            </ScrollView>
            {props.footerChildren ? (
              <View style={styles.padding}>
                <LinearGradient colors={COLORS} style={styles.linearGradient} />
                {props.footerChildren}
              </View>
            ) : null}
          </View>
        </View>
      </SafeAreaView>
    </RNModal>
  )
}

const styles = StyleSheet.create({
  contentContainer: {
    padding: CONTENT_SPACING - SCROLL_INDICATOR_OFFSET,
  },
  linearGradient: {
    backgroundColor: '#ffffff00',
    height: CONTENT_SPACING,
    left: CONTENT_SPACING,
    position: 'absolute',
    right: CONTENT_SPACING,
    top: -CONTENT_SPACING,
    zIndex: 1,
  },
  padding: {
    paddingBottom: CONTENT_SPACING,
    paddingHorizontal: CONTENT_SPACING,
  },
  safeLayout: {
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
  },
  scrollView: {
    margin: SCROLL_INDICATOR_OFFSET,
  },
  view: {
    flex: 0,
    flexGrow: 0,
    flexShrink: 1,
    marginBottom: 'auto',
    marginTop: 'auto',
    padding: CONTENT_SPACING,
  },
  view2: {
    backgroundColor: 'white',
    borderRadius: BORDER_RADIUS,
    flex: 0,
    flexGrow: 0,
    flexShrink: 1,
  },
})
```

I thought I'd document this as it was quite fiddly to get right. If you've had to implement something similar, I'd love to hear how you did it! I'm on [Twitter](https://twitter.com/bionicjulia) and [Instagram](https://instagram.com/bionicjulia).