Provider

The five layers DelacourProvider composes, in order, what breaks at each, and the one it leaves to you.

Mount DelacourProvider once, around everything.

app/_layout.tsx
import { DelacourProvider } from "@delacour/react-native-ui/provider";

<DelacourProvider>{children}</DelacourProvider>;

The five layers

Outermost first. The order is not stylistic.

LayerWhy it is where it is
GestureHandlerRootViewMust be an ancestor native view of every handler a Pressable creates. Its absence is silent — no error, presses simply stop landing.
SafeAreaProviderSeeded with initialWindowMetrics.
KeyboardProviderOwns one pair of shared animation values for the whole app.
<KeyboardStateSync />Must be a child of KeyboardProvider — it calls useKeyboardContext(). A sibling of the children, not a parent of them.
CalmMotionProviderPure context carrying isMotionCalm. Wraps the children alone.

One more layer is yours

An app that uses BottomSheet mounts BottomSheetProvider itself, inside DelacourProvider and around the navigator. It is the root host every modal sheet teleports to, and DelacourProvider cannot mount it: the sheet's engine, @delacour/react-native-bottom-sheet, is an optional peer, and an import here would make every app resolve it and build its native peer react-native-teleport, sheet or no sheet.

app/_layout.tsx
import { BottomSheetProvider } from "@delacour/react-native-ui/bottom-sheet";
import { DelacourProvider } from "@delacour/react-native-ui/provider";

<DelacourProvider>
  <BottomSheetProvider>{children}</BottomSheetProvider>
</DelacourProvider>;

Without it a sheet still renders — where it is written, as an inline sheet, under the navigator.

Why the safe-area seed matters

SafeAreaProvider renders null — not unstyled children, nothing — until its native view reports the first onInsetsChange. Without the seed, every cold start shows a blank frame.

The seed is a snapshot taken at native module init, so it is stale when the app launches into a rotated or split-screen window — stale for exactly one commit, because the native measurement overwrites it. A blank frame on every launch is the worse trade.

Pass initialMetrics={null} to opt out. A default parameter only fires on undefined, so null is a value rather than an absence.

Why the keyboard sync is required

KeyboardProvider owns exactly one pair of shared animation values for the whole app, and on iOS they are written only from the will events. Any teardown that skips one — an interactive dismiss interrupted by navigation, a stack pop, an app suspend — pins them open app-wide. Every screen then renders "keyboard open" over a keyboard that is not there.

Screen.Footer runs the same repair on mount as a backstop, but the provider-level sync is what keeps the values honest between screens.

style reaches the gesture root, and carries no default

GestureHandlerRootView applies its own { flex: 1 } whenever style is undefined. Pass a style and that flex: 1 is gone — include it yourself.

<DelacourProvider style={{ flex: 1, backgroundColor: "#000" }}>{children}</DelacourProvider>

isMotionCalm holds decorative loops still

For an E2E build. A test runner that waits for the screen to stop moving before each gesture — Argent, Detox and Maestro all do — pays its whole timeout on any screen holding a loop that never ends, so one shimmering Skeleton turns every tap there into a wait.

<DelacourProvider isMotionCalm={IS_E2E}>{children}</DelacourProvider>

IS_E2E is your app's own build flag. Decorative loops — a Skeleton's shimmer or pulse — then hold still exactly as they do under the OS reduce-motion setting. Motion that is the behaviour keeps moving: a Spinner still says work is in flight, an indeterminate Progress still sweeps.

Your own components ask the same question with useCalmMotion(), from @delacour/react-native-ui/hooks/use-calm-motion.

What it deliberately does not do

There are no per-layer escape hatches — no gestureHandler={false}, no safeAreaProps, no keyboardProps — and it does not detect an enclosing copy of itself. Nesting KeyboardProvider genuinely breaks: two pairs of shared values, two sets of native observers.

If you need a different stack, compose the providers by hand. They are all public from their own packages.

Composing the layers by hand

Only if your app already has a root stack of its own. <KeyboardStateSync /> is required, not optional polish, and must be a child of KeyboardProvider. CalmMotionProvider is optional — without it useCalmMotion() follows reduce motion alone. BottomSheetProvider goes innermost, and only if the app has sheets.

import { BottomSheetProvider } from "@delacour/react-native-ui/bottom-sheet";
import { CalmMotionProvider } from "@delacour/react-native-ui/hooks/use-calm-motion";
import { KeyboardStateSync } from "@delacour/react-native-ui/hooks/use-keyboard-state-sync";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context";

<GestureHandlerRootView>
  <SafeAreaProvider initialMetrics={initialWindowMetrics}>
    <KeyboardProvider>
      <KeyboardStateSync />
      <CalmMotionProvider isMotionCalm={IS_E2E}>
        <BottomSheetProvider>{children}</BottomSheetProvider>
      </CalmMotionProvider>
    </KeyboardProvider>
  </SafeAreaProvider>
</GestureHandlerRootView>;

On this page