Core

Every number the sheet computes — snap points, keyboard, footer, geometry, the step machine — as pure functions with no native module in the graph.

import { dynamicSnapPoint, keyboardLift, positionFor, UNMEASURED } from "@delacour/react-native-bottom-sheet/core";

@delacour/react-native-bottom-sheet/core is every number the package computes and nothing that renders. Nothing under it imports a runtime value from React, React Native, Reanimated, Gesture Handler, keyboard-controller, safe-area-context or teleport, which is what lets a skin re-export the footer's inset rules without mounting a sheet — and what lets the whole of it, the footer-on-keyboard proof included, run under bun test with no simulator.

import { footerTop, keyboardLift } from "@delacour/react-native-bottom-sheet/core";

const lift = keyboardLift({ keyboardHeight: 336, progress: 1, band: 34, behavior: "interactive", gapBelow: 0 });
const top = footerTop({ sheetHeight: 420 + lift, keyboardHeight: 336, footerContentHeight: 72, band: 0 });

Height space

Every value is a height: pixels of sheet visible above its resting bottom line. 0 is closed, snap points ascend, and UNMEASURED (-1) is a measurement that has not happened. translateY is derived from a height only at the moment a view needs it, by positionFor, and nowhere else.

The formulas

With C the container height, inset the safe-area bottom, kb the owned keyboard within the container and p keyboard-controller's progress:

restingBottom = detached ? bottomOffset + inset : 0          closedHeight = −restingBottom
available     = C − restingBottom                            maxHeight = available
band          = detached ? 0 : inset                         bandNow = band · (1 − p)
footerHeight  = hasFooter ? footerContent + bandNow : 0
dynamicSnapPoint = min(handle + content + (hasFooter ? footerContent : 0) + band, maxDynamicContentSize, available)
snapPoints       = sortAsc(unique(normalize(spec, available) ∪ {dynamicSnapPoint if dynamicSizing}))
keyboardLift  = behavior ∈ {interactive, extend} ? max(0, kb − band · p − (detached ? restingBottom : 0)) : 0
height        = clamp(base + keyboardLift, closedHeight, maxHeight)
translateY    = C − restingBottom − height
index         = piecewise(base, [closedHeight, …snapPoints] → [−1, 0, 1, …])      base, never height
contentArea   = max(0, min(maxHeight, highest + keyboardLift) − handle − footerHeight − kb − (hasFooter ? 0 : bandNow))
footerTop     = max(0, height − kb − footerContent − bandNow)
backdrop      = index ≥ appearsOn ? opacity : index ≤ disappearsOn ? 0 : linear

base is the one value a gesture or an animation writes — the keyboard-free height. Everything else is derived from it on the UI thread, one useDerivedValue per line above.

The flat worklets

Most functions here carry a "worklet" directive and run on the UI thread every frame of a drag. Each calls nothing and closes over nothing but module-scope constants — a module-scope worklet that called another would bind at import time and hand the UI thread undefined is not a function the moment a finger landed. A worklet you build inside a hook is captured by ordinary closure and may call any of them, which is how the pan composes resistOverDrag and selectSnapHeight without duplicating either.

A degenerate input returns a finite number, never NaN: one NaN written into base freezes a sheet permanently with nothing on screen to say why.

Snap points

ExportWhat it does
parseSnapPoint(spec)One SnapPointSpec to { kind: "px" | "percent", value }, as a Result
normalizeSnapPoints(spec, available)Ascending, unique, clamped; invalid entries skipped. Worklet
dynamicSnapPoint({ handleHeight, contentHeight, footerContentHeight, hasFooter, band, available, maxDynamicContentSize? })The content's own snap point. Worklet
indexForHeight(height, snapPoints, closedHeight), heightForIndex(index, snapPoints, closedHeight)Piecewise-linear over [closed, …snapPoints], clamped. Worklets
selectSnapHeight({ height, velocity, snapPoints, closedHeight, projection })The candidate nearest height + projection · velocity. Worklet
resistOverDrag(height, min, max, factor)Slope one at the edge, square root beyond; factor 0 clamps. Worklet
sheetState(base, height, snapPoints, closedHeight, maxHeight)SHEET_STATE.CLOSED | OPENED | EXTENDED | OVER_EXTENDED | FILL. Worklet

Keyboard

ExportWhat it does
keyboardInContainer(rawKeyboardHeight, containerBottomOffset)keyboard-controller's negative height to the positive overlap with the frame. Worklet
keyboardLift({ keyboardHeight, progress, band, behavior, gapBelow })The lift: keyboard less the band in step with progress, less a detached gap. Worklet
resolveKeyboardOwner(input)Whose keyboard it is, under keyboardScope, sticky through a transition
isInputInsideSheet(input)The inside scope's geometry test, in window space. Worklet
keyboardStep(input)What a behaviour does this frame: apply, restore, release or nothing
contentArea({ sheetHeight, handleHeight, footerHeight, keyboardHeight, trailingBand })What the body may fill, never negative. Worklet
acceptContainerLayout(input)Refuses Android's adjustResize double-count while a keyboard is up
shouldResetKeyboardAnimation(state)The stale keyboard-controller guard. Worklet
ExportWhat it does
bottomBand(detached, bottomInset), bandNow(band, progress), footerHeight(hasFooter, footerContentHeight, band)The band and what the footer occupies. Worklets
footerTop({ sheetHeight, keyboardHeight, footerContentHeight, band })The footer's offset inside the panel — constant across a keyboard animation. Worklet
resolveSheetBottomInset, resolveSheetScrollEndPaddingThe skin's padding rules, re-exported by it

Geometry

ExportWhat it does
restingBottom(detached, bottomOffset, bottomInset), closedHeight(restingBottom), availableHeight(containerHeight, restingBottom)Where the sheet rests, what closed is, what it may fill. Worklets
positionFor(containerHeight, restingBottom, height)Height to translateY, the only conversion. Worklet
clampHeight(height, closedHeight, maxHeight)NaN collapses to closed. Worklet
surfaceHeight(height, lowestSnapPoint, highestSnapPoint)What a detached card's surface is tall — between its snap points, the nearest snap point beyond them. Worklet
resolveDetached(prop), DETACHED_DEFAULTSThe detached prop to { horizontalMargin, bottomOffset } or null
detachedFrame({ containerWidth, horizontalMargin, bottomOffset, bottomInset }){ left, width, restingBottom }. Worklet

Backdrop, intents, haptics, scroll

ExportWhat it does
backdropOpacity(index, disappearsOn, appearsOn, opacity), backdropInteractive(index, disappearsOn)What the overlay draws at, and whether it takes touches. Worklets
resolveIntent(state, intent){ action: "wait" | "animate" | "jump" } or null — what a ref method does given where the sheet is. Worklet
isLayoutReady(input)Whether every measurement the configuration needs has landed. Worklet
crossedSnapPoint(lastIndex, height, snapPoints), snapPointUnder(height, snapPoints)Whether a frame's travel crossed a snap point, and which is under a height. Worklets
shouldLockScroll(state, enableContentPan), contentPanDrivesSheet({ locked, contentOffsetY, translationY })The scroll lock's two questions. Worklets
listDragHeight(input), listOwnsRelease({ scrollable, offset, base, highest }), scrollLockTarget(offset, byContentPan)The start offset as a budget the finger spends, whose the release is, where the lock holds. Worklets

Animation

ExportWhat it does
selectAnimation(config, platform, overrideReduceMotion)The consumer's config over the platform default, every field filled, as data
IOS_SPRING, ANDROID_TIMINGThe two defaults
SheetAnimationConfig, ResolvedAnimation, EasingName, ReduceMotionModeThe types

Machine

ExportWhat it does
defineSheetMachine(config){ config, initial, transition, can, steps, directionOf, nodeOf }
transition(states, snapshot, event)The one step, as a Result<Snapshot, SheetTransitionError> — never throws, never mutates
resolveTarget(target)A bare target or an object to { target, guard?, assign? }
stepFrame(transition, role, direction, progress, width)A step's opacity and offset mid-change, for crossfade, slide and none. Worklet
stepOverride(node)The snapPoints and dismissible a step asks of the root
SheetMachine, SheetMachineConfig, SheetStateNode, SheetMachineSnapshot, SheetTransitionErrorThe types

Vocabulary

ExportWhat it is
SnapPointSpecnumber | `${number}%`
KeyboardBehavior, KeyboardBlurBehavior, KeyboardScopeThe three keyboard unions
AnimationSource"mount" | "gesture" | "user" | "snapPoints" | "keyboard" | "container"
SheetIntentThe request a ref method writes, keyed by id so two identical requests both run
GESTURE_SOURCE, SHEET_STATE, SCROLLABLE_TYPENumeric enums — a shared value holding a string serialises on every write
UNMEASURED, CLOSED_INDEX-1 and -1
Result, ok, errThe house result union, copied in so the package depends on nothing private

On this page