npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-velora-sheet

v0.1.0

Published

Lightweight React Native bottom sheet with a measured bottom safe-area inset and a footer that lands exactly on the keyboard (iOS + Android). Ships no UI.

Readme

react-native-velora-sheet

A lightweight React Native bottom sheet that gets the bottom edge right: a safe-area inset measured inside the modal's own window, and a footer region that lands exactly on the keyboard — on iOS, on Android, and inside the modal window where most sheets get this wrong.

It ships no UI. No input, no button, no text, no theme, no colours you have to override. You pass components; the library only guarantees where they sit.

npm install react-native-velora-sheet react-native-safe-area-context
yarn add react-native-velora-sheet react-native-safe-area-context

No native module. No react-native-reanimated. No react-native-gesture-handler. Zero runtime dependencies.


Contents


Why

Most bottom sheets get the top of the sheet right and the bottom wrong. Two things break, over and over:

Content sinks under the home indicator or navigation bar. A sheet's last row — a submit button, a message input, the final list item — ends up half-hidden behind the system bar, because the sheet never measured the inset that applies to its own window.

The input hides behind the keyboard. Or it lifts too far and leaves a strip of dead space above the keys, because the maths mixed screen coordinates with window coordinates. On Android those two origins differ by the status bar, the navigation bar, or both.

Velora fixes exactly those two things and stays out of the way of everything else.


Advantages

No native dependencies. Nothing to link, no reanimated, no gesture-handler, no pods beyond react-native-safe-area-context. Drops into any project, including ones that deliberately avoid those libraries. Zero runtime dependencies of its own.

Correct bottom inset inside a modal. On Android a Modal is a separate window, so the app-level SafeAreaProvider describes the wrong thing. Velora nests a provider inside its own modal and reads that window's insets. Verified on a physical Android 15 device with gesture navigation.

The footer lands on the keyboard, not near it. Measured, not guessed. On a real device the footer's bottom edge and the IME's reported frame top were the same pixel.

Headless. The library renders a backdrop, a drag handle and three positioned regions. Everything visible inside them is yours, so it never fights your design system and there is no theme to override.

Tiny. 170 kB unpacked, one component, 38 props.

Fully typed. Ships TypeScript declarations; verified to resolve under bundler, node16 and nodenext module resolution.

Works outside a sheet too. The keyboard layer is exported on its own, so you can use the same measured avoidance on an ordinary screen.


Setup

Wrap your app in SafeAreaProvider once, as you normally would:

import { SafeAreaProvider } from 'react-native-safe-area-context';

export default function App() {
  return (
    <SafeAreaProvider>
      <RootNavigator />
    </SafeAreaProvider>
  );
}

That is the whole setup. The sheet nests its own provider internally for the modal window — you do not need to do anything about that.


Quick start

import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import { VeloraSheet } from 'react-native-velora-sheet';

export function Example() {
  const [open, setOpen] = useState(false);

  return (
    <View>
      <Button title="Open" onPress={() => setOpen(true)} />

      <VeloraSheet
        showModal={open}
        setShowModal={setOpen}
        snapPoints={[60, 90]}
        headerComponent={<Text style={{ padding: 16 }}>Title</Text>}
        footerComponent={<MyActionBar />}
      >
        <MyScrollableContent />
      </VeloraSheet>
    </View>
  );
}

Anatomy

┌─────────────────────────────┐
│        ▁▁▁▁▁▁▁▁             │  handleComponent   — drag target (or null)
│  headerComponent            │  fixed; ignores the keyboard
├─────────────────────────────┤
│                             │
│  children                   │  body — flexes, shrinks by the keyboard overlap
│                             │
├─────────────────────────────┤
│  footerComponent            │  rides the keyboard
│ ▁▁ bottom safe-area inset ▁▁│  applied here, dropped while the keyboard is up
└─────────────────────────────┘

Three regions, and they are all yours:

| Region | Behaviour | |---|---| | headerComponent | Fixed at the top of the sheet, below the handle. Never moves. | | children | The body. Flexes to fill the space, and shrinks by the keyboard overlap under the default keyboardBehavior. | | footerComponent | Pinned at the bottom. Rides the keyboard and carries the bottom safe-area inset. |

With no footerComponent the bottom inset moves to the body, so content still clears the home indicator.


Use cases

A comments or chat sheet

The body scrolls, the composer stays on the keyboard, and the list stays anchored to the composer because keyboardBehavior="avoid" shrinks the body rather than sliding it.

const [text, setText] = useState('');
const canSend = text.trim().length > 0;

<VeloraSheet
  showModal={open}
  setShowModal={setOpen}
  snapPoints={[90]}
  headerComponent={<Header title="Comments" onClose={() => setOpen(false)} />}
  footerComponent={
    <View style={styles.composer}>
      <TextInput
        style={styles.input}
        value={text}
        onChangeText={setText}
        placeholder="Write a comment…"
        multiline
      />
      <Pressable onPress={send} disabled={!canSend}>
        <SendIcon />
      </Pressable>
    </View>
  }
>
  <FlatList
    data={comments}
    renderItem={renderComment}
    keyboardShouldPersistTaps="handled"
    keyboardDismissMode="interactive"
  />
</VeloraSheet>

Add inverted to the FlatList for a chat-style feed that grows upward.

A form sheet with a submit button

The button clears the home indicator when the keyboard is closed and sits on the keyboard when it is open — without you writing a single inset calculation.

<VeloraSheet
  showModal={open}
  setShowModal={setOpen}
  snapPoints={[75]}
  headerComponent={<Text style={styles.title}>Add address</Text>}
  footerComponent={
    <Pressable style={styles.cta} onPress={submit}>
      <Text style={styles.ctaText}>Save</Text>
    </Pressable>
  }
>
  <ScrollView keyboardShouldPersistTaps="handled">
    <TextInput placeholder="Street" />
    <TextInput placeholder="City" />
    <TextInput placeholder="Postcode" />
  </ScrollView>
</VeloraSheet>

A filter or sort sheet

No keyboard involved, so switch the handling off and let the sheet size itself to two snap points.

<VeloraSheet
  showModal={open}
  setShowModal={setOpen}
  snapPoints={[45, 85]}
  keyboardBehavior="none"
  headerComponent={<Text style={styles.title}>Filters</Text>}
  footerComponent={
    <View style={styles.row}>
      <Pressable onPress={reset}><Text>Reset</Text></Pressable>
      <Pressable onPress={apply}><Text>Apply</Text></Pressable>
    </View>
  }
>
  <FilterList />
</VeloraSheet>

A simple action / share menu

No footer at all — the bottom inset moves to the body automatically.

<VeloraSheet
  showModal={open}
  setShowModal={setOpen}
  snapPoints={[35]}
  dragArea="full"
  handleComponent={null}
  borderRadius={28}
>
  <ActionRow icon="share" label="Share" onPress={share} />
  <ActionRow icon="link" label="Copy link" onPress={copy} />
  <ActionRow icon="flag" label="Report" onPress={report} />
</VeloraSheet>

A checkout / payment summary

A sticky total bar over content that does not scroll.

<VeloraSheet
  showModal={open}
  setShowModal={setOpen}
  snapPoints={[80]}
  keyboardBehavior="sticky"
  footerComponent={<PayBar total={total} onPay={pay} />}
>
  <OrderSummary items={items} />
</VeloraSheet>

Imperative control

Open and close from anywhere without holding visibility in state.

const sheet = useRef<VeloraSheetRef>(null);

<VeloraSheet ref={sheet} snapPoints={[40, 90]}>
  <Content />
</VeloraSheet>

sheet.current?.open();
sheet.current?.snapToIndex(1);
sheet.current?.close();

Reading the sheet's own insets from your components

Anything rendered inside the sheet can read the measured inset and the sheet controls.

import { useVeloraSheet, useIsKeyboardVisible } from 'react-native-velora-sheet';

function MyFooter() {
  const { safeAreaBottom, insets, close, snapToIndex } = useVeloraSheet();
  const keyboardUp = useIsKeyboardVisible();
  return <View>{/* … */}</View>;
}

Props

Visibility

| Prop | Type | Default | Description | |---|---|---|---| | showModal | boolean | — | Controlled visibility. | | setShowModal | (visible: boolean) => void | — | Called when the sheet opens or closes itself. | | visible | boolean | — | Alias for showModal. | | onVisibleChange | (visible: boolean) => void | — | Alias for setShowModal. |

Layout and snap points

| Prop | Type | Default | Description | |---|---|---|---| | snapPoints | (number \| string)[] | [90] | Heights as a percentage of the available space: 50 or '50%'. Sorted ascending, so index 0 is always the smallest. | | initialSnapIndex | number | 0 | Snap index the sheet opens at. | | topInset | number | 0 | Space reserved above the sheet's maximum height. |

Bottom edge and safe area

| Prop | Type | Default | Description | |---|---|---|---| | useSafeArea | boolean | true | Apply the measured bottom safe-area inset. | | bottomInset | number | — | Override the measured inset with a fixed value. | | navigationBarTranslucent | boolean | false | Let the Android dialog draw under the navigation bar. |

Keyboard

| Prop | Type | Default | Description | |---|---|---|---| | keyboardBehavior | 'avoid' \| 'sticky' \| 'none' | 'avoid' | avoid shrinks the body so a scrollable body stays anchored to the footer. sticky translates only the footer. none disables keyboard handling. | | keyboardExtraSpace | number | 0 | Extra dp kept above the keyboard. | | dismissKeyboardOnClose | boolean | true | Dismiss the keyboard as the sheet closes. |

Gestures and dismissal

| Prop | Type | Default | Description | |---|---|---|---| | dragArea | 'handle' \| 'full' \| 'none' | 'handle' | Which region responds to the drag gesture. | | enablePanDownToClose | boolean | true | Swipe down past the smallest snap point to close. | | closeOnBackdropPress | boolean | true | Tap outside the sheet to close. When false, no invisible layer is mounted, so touches pass through. | | closeOnBackButton | boolean | true | Android hardware back closes the sheet. |

Slots

| Prop | Type | Description | |---|---|---| | children | ReactNode | The body. | | headerComponent | ReactNode | Fixed above the body. | | footerComponent | ReactNode | Pinned below the body; rides the keyboard and carries the bottom inset. | | handleComponent | ReactNode \| null | Replace the drag handle. null removes it. | | backdropComponent | ReactNode \| null | Replace the dimmed backdrop. null removes it. |

Appearance

| Prop | Type | Default | Description | |---|---|---|---| | backgroundColor | string | '#FFFFFF' | Sheet background. | | backdropColor | string | '#000000' | Backdrop colour. | | backdropOpacity | number | 0.45 | Backdrop opacity when fully open. | | borderRadius | number | 16 | Top corner radius. | | style | ViewStyle | — | The sheet surface. | | contentContainerStyle | ViewStyle | — | Wraps children only. | | footerStyle | ViewStyle | — | Wraps footerComponent only. | | handleStyle | ViewStyle | — | The default handle's container. | | handleIndicatorStyle | ViewStyle | — | The default handle's bar. |

Animation

| Prop | Type | Default | Description | |---|---|---|---| | openDuration | number | 280 | Open animation duration in ms. | | closeDuration | number | 220 | Close animation duration in ms. |

Callbacks

| Prop | Type | Description | |---|---|---| | onOpen | () => void | Fires when the open animation finishes. | | onClose | () => void | Fires when the close animation finishes. | | onSnapPointChange | (index: number) => void | Fires when the sheet settles on a snap point. |

Platform and misc

| Prop | Type | Default | Description | |---|---|---|---| | supportedOrientations | string[] | all four | iOS only. React Native's Modal defaults to portrait-only; this defaults to all four and lets your app's own orientation lock decide. | | testID | string | — | Applied to the underlying Modal. |


Ref methods

const sheet = useRef<VeloraSheetRef>(null);

| Method | Description | |---|---| | open() | Open at initialSnapIndex. | | close() | Animate closed and notify setShowModal / onClose. | | snapToIndex(index) | Move to a snap point. Clamped to the available range. |


Hooks and utilities

The keyboard layer is exported on its own and works anywhere, not just inside a sheet.

| Export | Description | |---|---| | useVeloraSheet() | Inside a sheet: { insets, safeAreaBottom, close, snapToIndex }. | | useKeyboard() | Full metrics: { height, screenY, duration, easing, state, isVisible }. | | useIsKeyboardVisible() | boolean. | | useKeyboardHeight() | Coverage in dp, measured up from the screen bottom. | | KeyboardAvoidingContainer | Flex container that shrinks by however much of it the keyboard covers. | | KeyboardStickyView | Translates its children above the keyboard, native driver. | | KeyboardSource | The single process-wide keyboard subscription. subscribe() / getMetrics(). | | parseSnapPercent · snapPointsToHeights · nearestSnapIndex | Pure snap-point helpers. |

import { KeyboardAvoidingContainer } from 'react-native-velora-sheet';

// Same measured avoidance on an ordinary screen
<KeyboardAvoidingContainer style={{ flex: 1 }}>
  <MessageList />
  <Composer />
</KeyboardAvoidingContainer>

How the keyboard handling works

A footer has to land on the keyboard's top edge, and that edge is reported in screen coordinates while almost every layout calculation reaches for Dimensions.get('window'). Those two origins are not the same:

| Configuration | window origin vs screen | Error if you mix them | |---|---|---| | iOS | identical | none | | Android, opaque status bar | 24dp down | footer floats above the keys | | Android, translucent modal | window.height still excludes the nav bar | footer sits too high | | Android edge-to-edge | identical | none |

Velora never mixes them. screenY — the keyboard's top edge — is the primary measurement, and every layout decision compares it against a measureInWindow result, which is in the same coordinate space on both platforms and inside a modal:

overlap = (container.y + container.height) − keyboard.screenY

That one line is correct in all four cases the library has to survive:

  • iOS — the modal window never resizes, so the full overlap is applied.
  • Android with SOFT_INPUT_ADJUST_RESIZE (what React Native sets on every modal dialog) — the OS already lifted the window, the measured bottom moved up with it, the overlap resolves to ~0, and nothing is double-counted.
  • Android without a resize — the overlap is the whole IME, navigation bar included, and the container absorbs it.
  • No keyboard event reaches JavaScript at all — the Android window resize alone still puts the footer in the right place, because the layout is flex-based rather than event-driven.

Why the insets are measured twice

On Android a Modal is a separate window. The app-level SafeAreaProvider describes the activity, not the dialog, so reading useSafeAreaInsets() outside the sheet gives the wrong bottom inset — a phantom gap when the dialog already stops above an opaque navigation bar, or no padding at all when it draws edge to edge.

VeloraSheet therefore nests a SafeAreaProvider inside its Modal. react-native-safe-area-context reports only the portion of a system bar that actually overlaps the provider view, which resolves to:

| | keyboard closed | keyboard open | |---|---|---| | iOS | 34 (home indicator) | 0 | | Android, opaque nav bar | 0 (window already inset) | 0 | | Android, edge-to-edge | 48 (nav bar) | 0 |

The inset drops to 0 while the keyboard is open because the home indicator and navigation bar are behind the keyboard by then; keeping it would leave a visible strip between the footer and the keys.


Limitations

Worth knowing before you choose this over a heavier sheet:

  • Gestures run on the JavaScript thread. Velora uses PanResponder so it can avoid a react-native-gesture-handler dependency. The animation itself is native-driven, but under heavy JS load dragging can stutter. Libraries built on gesture-handler run the gesture on the UI thread and drag more smoothly.
  • No scroll-to-drag handoff. Dragging a scrollable body downward does not hand off to the sheet. This is why dragArea defaults to 'handle'; use 'full' only when the body does not scroll.
  • Percentage snap points only. There is no content-measured or dynamic sizing.
  • Modal-based. The sheet renders inside a React Native Modal, so nothing can overlap outside it and nested modals behave the way they do on iOS.
  • No detached, persistent or non-modal presentation modes.
  • No reduced-motion handling — animation durations are not shortened when the OS asks for reduced motion.

If you need UI-thread gestures, scroll handoff or dynamic sizing, use a reanimated-based sheet instead. If you want a small dependency-free sheet whose bottom edge is correct, this is the trade Velora makes.


Troubleshooting

The footer sits too high or too low on Android. Check whether your app is edge-to-edge. With an opaque navigation bar the dialog window is already inset and the measured bottom inset is correctly 0. Set navigationBarTranslucent only if you want the sheet to draw under the navigation bar.

Nothing appears when I open the sheet. Make sure a SafeAreaProvider wraps your app. Without it the nested provider still works, but seeding the first frame relies on initialWindowMetrics.

Dragging fights my list. Keep dragArea="handle" (the default). 'full' claims downward drags anywhere on the sheet.

The keyboard covers my input. Confirm keyboardBehavior is not 'none', and that your input is inside footerComponent rather than in the body.

Tapping outside does not close the sheet. closeOnBackdropPress must be true, and any custom backdropComponent must not swallow touches.


Full text reference

Every prop, export, behaviour rule, recipe and limitation is also written out in plain text in llms.txt, shipped inside the package at:

node_modules/react-native-velora-sheet/llms.txt

It is a single self-contained file with no markup, intended to be read directly or pasted into an AI assistant so it can answer questions about this library accurately. Nothing in it is generated at runtime — it is kept in step with the source by hand.

cat node_modules/react-native-velora-sheet/llms.txt

Peer dependencies

| Package | Range | |---|---| | react | >=18.0.0 | | react-native | >=0.72.0 | | react-native-safe-area-context | >=4.0.0 |


License

MIT