Provider
The five layers DelacourProvider composes, in order, what breaks at each, and the one it leaves to you.
Mount DelacourProvider once, around everything.
import { DelacourProvider } from "@delacour/react-native-ui/provider";
<DelacourProvider>{children}</DelacourProvider>;The five layers
Outermost first. The order is not stylistic.
| Layer | Why it is where it is |
|---|---|
GestureHandlerRootView | Must be an ancestor native view of every handler a Pressable creates. Its absence is silent — no error, presses simply stop landing. |
SafeAreaProvider | Seeded with initialWindowMetrics. |
KeyboardProvider | Owns 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. |
CalmMotionProvider | Pure 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.
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>;