• Stars
    star
    939
  • Rank 48,667 (Top 1.0 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created about 4 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Accessible โ™ฟ๏ธ, Delightful โœจ, & Fast ๐Ÿš€

npm stat npm version gzip size size module formats: cjs, es, and modern

Logo with the text Accessible, Delightful and Performant

react-spring-bottom-sheet is built on top of react-spring and react-use-gesture. It busts the myth that accessibility and supporting keyboard navigation and screen readers are allegedly at odds with delightful, beautiful, and highly animated UIs. Every animation and transition use CSS custom properties instead of manipulating them directly, allowing complete control over the experience from CSS alone.

Installation

npm i react-spring-bottom-sheet

Getting started

Basic usage

import { useState } from 'react'
import { BottomSheet } from 'react-spring-bottom-sheet'

// if setting up the CSS is tricky, you can add this to your page somewhere:
// <link rel="stylesheet" href="https://unpkg.com/react-spring-bottom-sheet/dist/style.css" crossorigin="anonymous">
import 'react-spring-bottom-sheet/dist/style.css'

export default function Example() {
  const [open, setOpen] = useState(false)
  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      <BottomSheet open={open}>My awesome content here</BottomSheet>
    </>
  )
}

TypeScript

TS support is baked in, and if you're using the snapTo API use BottomSheetRef:

import { useRef } from 'react'
import { BottomSheet, BottomSheetRef } from 'react-spring-bottom-sheet'

export default function Example() {
  const sheetRef = useRef<BottomSheetRef>()
  return (
    <BottomSheet open ref={sheetRef}>
      <button
        onClick={() => {
          // Full typing for the arguments available in snapTo, yay!!
          sheetRef.current.snapTo(({ maxHeight }) => maxHeight)
        }}
      >
        Expand to full height
      </button>
    </BottomSheet>
  )
}

Customizing the CSS

Using CSS Custom Properties

These are all the variables available to customize the look and feel when using the provided CSS.

:root {
  --rsbs-backdrop-bg: rgba(0, 0, 0, 0.6);
  --rsbs-bg: #fff;
  --rsbs-handle-bg: hsla(0, 0%, 0%, 0.14);
  --rsbs-max-w: auto;
  --rsbs-ml: env(safe-area-inset-left);
  --rsbs-mr: env(safe-area-inset-right);
  --rsbs-overlay-rounded: 16px;
}

Custom CSS

It's recommended that you copy from style.css into your own project, and add this to your postcss.config.js setup (npm i postcss-custom-properties-fallback):

module.exports = {
  plugins: {
    // Ensures the default variables are available
    'postcss-custom-properties-fallback': {
      importFrom: require.resolve('react-spring-bottom-sheet/defaults.json'),
    },
  },
}

Demos

Basic

View demo code

MVP example, showing what you get by implementing open, onDismiss and a single snap point always set to minHeight.

Snap points & overflow

View demo code

A more elaborate example that showcases how snap points work. It also shows how it behaves if you want it to be open by default, and not closable. Notice how it responds if you resize the window, or scroll to the bottom and starts adjusting the height of the sheet without scrolling back up first.

Sticky header & footer

View demo code

If you provide either a header or footer prop you'll enable the special behavior seen in this example. And they're not just sticky positioned, both areas support touch gestures.

Non-blocking overlay mode

View demo code

In most cases you use a bottom sheet the same way you do with a dialog: you want it to overlay the page and block out distractions. But there are times when you want a bottom sheet but without it taking all the attention and overlaying the entire page. Providing blocking={false} helps this use case. By doing so you disable a couple of behaviors that are there for accessibility (focus-locking and more) that prevents a screen reader or a keyboard user from accidentally leaving the bottom sheet.

API

props

All props you provide, like className, style props or whatever else are spread onto the underlying <animated.div> instance, that you can style in your custom CSS using this selector: [data-rsbs-root]. Just note that the component is mounted in a @reach/portal at the bottom of <body>, and not in the DOM hierarchy you render it in.

open

Type: boolean

The only required prop, beyond children. And it's controlled, so if you don't set this to false then it's not possible to close the bottom sheet. It's worth knowing that the bottom sheet won't render anything but a @reach/dialog placeholder while open is false. Thus ensure your components behave as expected with being unmounted when the sheet closed. We can't really allow it to render and mount while in a closed/hidden position as there's no stable way of preventing keyboard users or screen readers from accidentally interacting with the closed bottom sheet as long as it's in the dom. This is especially problematic given it implements ARIA to optimize for a11y.

onDismiss

Type: () => void

Called when the user do something that signal they want to dismiss the sheet:

  • hit the esc key.
  • tap on the backdrop.
  • swipes the sheet to the bottom of the viewport.

snapPoints

Type: (state) => number | number[]

This function should be pure as it's called often. You can choose to provide a single value or an array of values to customize the behavior. The state contains these values:

  • headerHeight โ€“ the current measured height of the header.
  • footerHeight โ€“ if a footer prop is provided then this is its height.
  • height โ€“ the current height of the sheet.
  • minHeight โ€“ the minimum height needed to avoid a scrollbar. If there's not enough height available to avoid it then this will be the same as maxHeight.
  • maxHeight โ€“ the maximum available height on the page, equivalent to window.innerHeight and 100vh.
<BottomSheet
  // Allow the user to select between minimun height to avoid a scrollbar, and fullscren
  snapPoints={({ minHeight, maxHeight }) => [minHeight, maxHeight]}
/>

defaultSnap

Type: number | (state) => number

Provide either a number, or a callback returning a number for the default position of the sheet when it opens. state use the same arguments as snapPoints, plus two more values: snapPoints and lastSnap.

<BottomSheet
  // the first snap points height depends on the content, while the second one is equivalent to 60vh
  snapPoints={({ minHeight, maxHeight }) => [minHeight, maxHeight / 0.6]}
  // Opens the largest snap point by default, unless the user selected one previously
  defaultSnap={({ lastSnap, snapPoints }) =>
    lastSnap ?? Math.max(...snapPoints)
  }
/>

header

Type: ReactNode

Supports the same value type as the children prop.

footer

Type: ReactNode

Supports the same value type as the children prop.

sibling

Type: ReactNode

Supports the same value type as the sibling prop. Renders the node as a child of [data-rsbs-root], but as a sibling to [data-rsbs-backdrop] and [data-rsbs-overlay]. This allows you to access the animation state and render elements on top of the bottom sheet, while being outside the overlay itself.

initialFocusRef

Type: React.Ref | false

A react ref to the element you want to get keyboard focus when opening. If not provided it's automatically selecting the first interactive element it finds. If set to false keyboard focus when opening is disabled.

blocking

Type: boolean

Enabled by default. Enables focus trapping of keyboard navigation, so you can't accidentally tab out of the bottom sheet and into the background. Also sets aria-hidden on the rest of the page to prevent Screen Readers from escaping as well.

scrollLocking

Type: boolean

iOS Safari, and some other mobile culprits, can be tricky if you're on a page that has scrolling overflow on document.body. Mobile browsers often prefer scrolling the page in these cases instead of letting you handle the touch interaction for UI such as the bottom sheet. Thus it's enabled by default. However it can be a bit agressive and can affect cases where you're putting a drag and drop element inside the bottom sheet. Such as <input type="range" /> and more. For these cases you can wrap them in a container and give them this data attribute [data-body-scroll-lock-ignore] to prevent intervention. Really handy if you're doing crazy stuff like putting mapbox-gl widgets inside bottom sheets.

expandOnContentDrag

Type: boolean

Disabled by default. By default, a user can expand the bottom sheet only by dragging a header or the overlay. This option enables expanding the bottom sheet on the content dragging.

Events

All events receive SpringEvent as their argument. The payload varies, but type is always present, which can be 'OPEN' | 'RESIZE' | 'SNAP' | 'CLOSE' depending on the scenario.

onSpringStart

Type: (event: SpringEvent) => void

Fired on: OPEN | RESIZE | SNAP | CLOSE.

If you need to delay the open animation until you're ready, perhaps you're loading some data and showing an inline spinner meanwhile. You can return a Promise or use an async function to make the bottom sheet wait for your work to finish before it starts the open transition.

function Example() {
  const [data, setData] = useState([])
  return (
    <BottomSheet
      onSpringStart={async (event) => {
        if (event.type === 'OPEN') {
          // the bottom sheet gently waits
          const data = await fetch(/* . . . */)
          setData(data)
          // and now we can proceed
        }
      }}
    >
      {data.map(/* . . . */)}
    </BottomSheet>
  )
}

onSpringCancel

Type: (event: SpringEvent) => void

Fired on: OPEN | CLOSE.

OPEN

In order to be as fluid and delightful as possible, the open state can be interrupted and redirected by the user without waiting for the open transition to complete. Maybe they changed their mind and decided to close the sheet because they tapped a button by mistake. This interruption can happen in a number of ways:

  • the user swipes the sheet below the fold, triggering an onDismiss event.
  • the user hits the esc key, triggering an onDismiss event.
  • the parent component sets open to false before finishing the animation.
  • a RESIZE event happens, like when an Android device shows its soft keyboard when an text editable input receives focus, as it changes the viewport height.

CLOSE

If the user reopens the sheet before it's done animating it'll trigger this event. Most importantly though it can fire if the bottom sheet is unmounted without enough time to clean animate itself out of the view before it rolls back things like body-scroll-lock, focus-trap and more. It'll still clean itself up even if React decides to be rude about it. But this also means that the event can fire after the component is unmounted, so you should avoid calling setState or similar without checking for the mounted status of your own wrapper component.

RESIZE

Type: { source: 'window' | 'maxheightprop' | 'element }

Fires whenever there's been a window resize event, or if the header, footer or content have changed its height in such a way that the valid snap points have changed. source tells you what caused the resize. If the resize comes from a window.onresize event it's set to 'window'. 'maxheightprop' is if the maxHeight prop is used, and is fired whenever it changes. And 'element' is whenever the header, footer or content resize observers detect a change.

SNAP

Type: { source: 'dragging' | 'custom' | string }

Fired after dragging ends, or when calling ref.snapTo, and a transition to a valid snap point is happening.

source is 'dragging' if the snapping is responding to a drag gesture that just ended. And it's set to 'custom' when using ref.snapTo.

function Example() {
  return (
    <BottomSheet
      onSpringStart={(event) => {
        if (event.type === 'SNAP' && event.source === 'dragging') {
          console.log('Starting a spring animation to user selected snap point')
        }
      }}
    />
  )
}

When using snapTo it's possible to use a different source than 'custom':

function Example() {
  const sheetRef = useRef()
  return (
    <BottomSheet
      ref={sheetRef}
      snapPoints={({ minHeight, maxHeight }) => [minHeight, maxHeight]}
      onSpringEnd={(event) => {
        if (event.type === 'SNAP' && event.source === 'snap-to-bottom') {
          console.log(
            'Just finished an imperativ transition to the bottom snap point'
          )
        }
      }}
    >
      <button
        onClick={() => sheetRef.current.snapTo(0, { source: 'snap-to-bottom' })}
      >
        Snap to bottom
      </button>
    </BottomSheet>
  )
}

onSpringEnd

Type: (event: SpringEvent) => void

Fired on: CLOSE.

The yin to onSpringStart's yang. It has the same characteristics. Including async/await and Promise support for delaying a transition. For CLOSE it gives you a hook into the step right after it has cleaned up everything after itself, and right before it unmounts itself. This can be useful if you have some logic that needs to perform some work before it's safe to unmount.

skipInitialTransition

Type: boolean

By default the initial open state is always transitioned to using an spring animation. Set skipInitialTransition to true and the initial open state will render as if it were the default state. Useful to avoid scenarios where the opening transition would be distracting.

ref

Methods available when setting a ref on the sheet:

export default function Example() {
  const sheetRef = React.useRef()
  return <BottomSheet open ref={sheetRef} />
}

snapTo

Type: (numberOrCallback: number | (state => number)) => void, options?: {source?: string, velocity?: number}

Same signature as the defaultSnap prop, calling it will animate the sheet to the new snap point you return. You can either call it with a number, which is the height in px (it'll select the closest snap point that matches your value): ref.current.snapTo(200). Or:

ref.current.snapTo(({ // Showing all the available props
  headerHeight, footerHeight, height, minHeight, maxHeight, snapPoints, lastSnap }) =>
  // Selecting the largest snap point, if you give it a number that doesn't match a snap point then it'll
  // select whichever snap point is nearest the value you gave
  Math.max(...snapPoints)
)

There's an optional second argument you can use to override event.source, as well as changing the velocity:

ref.current.snapTo(({ snapPoints }) => Math.min(...snapPoints), {
  // Each property is optional, here showing their default values
  source: 'custom',
  velocity: 1,
})

height

Type: number

The current snap point, in other words the height, of the bottom sheet. This value is updated outside the React render cycle, for performance reasons.

export default function Example() {
  const sheetRef = React.useRef()
  return (
    <BottomSheet
      ref={sheetRef}
      onSpringStart={() => {
        console.log('Transition from:', sheetRef.current.height)
        requestAnimationFrame(() =>
          console.log('Transition to:', sheetRef.current.height)
        )
      }}
      onSpringEnd={() =>
        console.log('Finished transition to:', sheetRef.current.height)
      }
    />
  )
}

Credits

More Repositories

1

scroll-into-view-if-needed

Element.scrollIntoView ponyfills for things like "if-needed" and "smooth"
JavaScript
1,065
star
2

ioredis-mock

Emulates ioredis by performing all operations in-memory.
JavaScript
337
star
3

compute-scroll-into-view

Utility for calculating what should be scrolled, how it's scrolled is up to you
TypeScript
199
star
4

smooth-scroll-into-view-if-needed

Smoothly scroll elements into view, cross browser!
TypeScript
136
star
5

uikit-react

UIkit components built with React
JavaScript
111
star
6

bulma-loader

A Webpack loader for Bulma, a modern CSS framework based on Flexbox
JavaScript
34
star
7

redux-saga-sc

Provides sagas to easily dispatch redux actions over SocketCluster websockets
JavaScript
34
star
8

String.Slugify.js

Extends the String native object to have a slugify method, useful for url slugs.
JavaScript
27
star
9

epic

React example project, that takes you from fun development to high quality production
TypeScript
21
star
10

groqz

(experimental) Transforms GROQ strings to zod schemas in your TypeScript codebase.
TypeScript
14
star
11

nextjs-cv-cms-sanity-v3

My over-engineered CV
TypeScript
11
star
12

String.Inflector.js

Extends the String native with inflector methods, like pluralize and singularize.
9
star
13

uikit-loader

A Webpack CSS loader for UIkit, a lightweight and modular front-end framework for developing fast and powerful web interfaces
JavaScript
9
star
14

Element.Style.Transform.js

Provides a cross browser way of letting you use the CSS3 transform property. Inspired by http://github.com/zachstronaut/jquery-css-transform
JavaScript
9
star
15

public-talks

Slides and other stuff is all in this repo. Each talk is on its own branch
7
star
16

svgdiff

See the visual difference between two SVGs
TypeScript
7
star
17

redux-form-uikit

A set of wrapper components to facilitate using UIkit React with Redux Form
JavaScript
5
star
18

cocody.dev

My own website, wanna do some small blogging and share my fav resources.
JavaScript
3
star
19

react-transform-count-renders

React Transform that lets you console.count how many times your React components render
JavaScript
3
star
20

sanity-meetup-08-22

"v3 does what v2 don't"
TypeScript
3
star
21

system-font-stack

Give your web app a native look by using the font family of the users OS
TypeScript
3
star
22

trains

choo choo ๐Ÿš‚
JavaScript
2
star
23

postcss-import-svg

JavaScript
2
star
24

example-v3-studio

TypeScript
2
star
25

postcss-custom-properties-fallback

Adds fallbacks to your CSS var() functions
JavaScript
2
star
26

next-sanity-preview

JavaScript
2
star
27

express-pretty-error

Express compatible middleware for pretty errors in html, json, raw text, css and terminal contexts with stack traces included!
JavaScript
2
star
28

template-marketing-webapp-nextjs

TypeScript
1
star
29

serve-dynamic-favicon

middleware that serve dynamically generated favicons
JavaScript
1
star
30

example-npm

An example GitHub Action using npm
JavaScript
1
star
31

stipsan.me

My own personal website, don't really need one but it's a good excuse to try new tools and experiment ๐Ÿš€
JavaScript
1
star
32

cancelpineapple.pizza

HTML
1
star
33

hyperfokus

Hyperfocus your todos until they're done
TypeScript
1
star
34

themer

Create Sanity Studio v3 themes!
TypeScript
1
star
35

graphql-field-resolver-to-typescript

Export ts definitions from your server .graphql files to strictly type your field resolvers
TypeScript
1
star
36

stipsan

1
star
37

scroll-into-view.dev

TypeScript
1
star
38

links

Because browser bookmarks sucks
1
star
39

renovate-presets

JavaScript
1
star
40

potatoes.fyi

HTML
1
star
41

top-github-code-reviewers

Showcase your most active code reviewers
JavaScript
1
star
42

nextjs-blog-cms-sanity-v3-netlify-test

TypeScript
1
star
43

redux-saga-sc-demo

A demo chat app showing redux-saga-sc in action
JavaScript
1
star
44

vc-app

TypeScript
1
star
45

gulp-purge-sourcemaps

Cleans up after gulp-sourcemaps have done a sourcemaps.write() allowing you to combine streams that generate both dev assets with sourcemaps and minified production assets.
JavaScript
1
star
46

sanity-template-gatsby-lps

JavaScript
1
star
47

example-v3-studio-next-runtime

A fork of stipsan/example-v3-studio to get around Vercel's "3 linked projects per repo" limit
TypeScript
1
star