Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 42 additions & 34 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ to the `Pick` semver-locks it.

```ts
type OpenPayload =
| { kind: 'inline'; id; groupId; content: ReactNode; scaleBackground?; backdrop?; params? }
| { kind: 'portal'; id; groupId; scaleBackground?; backdrop?; params? };
| { kind: 'inline'; id; groupId; content: ReactNode; scaleBackground?; params? }
| { kind: 'portal'; id; groupId; scaleBackground?; params? };
```

`kind` is what callers reason about, `usePortal` is what the renderer checks;
Expand All @@ -94,6 +94,13 @@ Key actions:
group's top and the one below is `hidden` — undoing a `switch`.
- `finishClosing(id)` hides if `keepMounted`, removes otherwise.
- `clearGroup` / `clearAll` are teardown: no animation, interceptors skipped.
- `setBackdrop(id, false | true | BackdropConfig)` is the **only** writer of the
record's `backdrop` field — `open()` never touches it, which is what lets a
persistent sheet's config survive re-open cycles. `true` means *clear the
override*, not a stored flag. The action bails on value-equal writes
(`backdropValuesEqual`): adapters re-apply their `backdrop` prop with a fresh
object literal on every consumer render, and without the bail each render
would wake every store subscriber.

**Modes:** `push` keeps the previous sheet visible, `switch` hides it (restored
on close), `replace` closes it.
Expand Down Expand Up @@ -195,6 +202,31 @@ whole stack above arbitrary app chrome — without it any host view with a modes
`animatedIndex`. Do not add a timer or delay gate: deferring the mount drops the
opening frames the adapter already drove, and the backdrop pops in mid-fade.

The backdrop's *look* is configurable (`BackdropConfig`, a `kind: 'styled' |
'custom'` union): group default via `backdrop` on the provider, per sheet
via the `backdrop` prop on the adapter (routed through `useAdapterBackdrop` →
`setBackdrop`). Resolution is **atomic for the visual choice** — a sheet-level
config replaces the group's rendering entirely; only `pressToDismiss` resolves
per field, and styles compose (`[default, group, sheet]`) when both levels are
`styled`. The adapter prop lands via effect a beat after the backdrop first
mounts, so it is applied in a *layout* effect: `animatedIndex` starts at `-1`,
which holds a `styled` backdrop at zero opacity for that frame, but a `custom`
one owns its own fade and would otherwise paint at full strength before the
sheet's config replaced it. The guarantee is structural for `styled` and
contractual for `custom`. A `kind: 'custom'` component owns its own fade
off `animatedIndex` — the built-in opacity is deliberately not applied on top.

Two selectors read the field, and the split is deliberate: `QueueItem` takes
`useSheetBackdropEnabled` (a boolean — "render one at all") so restyling does
not re-render the memoized sheet layer, and only `BottomSheetBackdrop` takes the
config through `useSheetBackdrop`.

Every shipped adapter exposes `backdrop?: BackdropConfig | false` (via the
shared `AdapterBackdropProps`) and, where its library draws an overlay of its
own, forces that overlay off. Re-exposing the underlying prop (gorhom's
`backdropComponent`, actions-sheet's overlay) would let a second, non-stack-aware
overlay paint over the manager's.

`useSheetRenderData` orders hidden persistent sheets before active ones so React
does not unmount and remount across transitions.

Expand Down Expand Up @@ -237,7 +269,8 @@ Public so a third-party adapter reaches parity with the shipped ones:
| `useAdapterRef(forwardedRef)` | resolves the ref context (portal/persistent) or the forwarded one (inline) |
| `useAnimatedIndex()` | the sheet's shared value, `-1` hidden → `0` visible |
| `useBackHandler(id, onBackPress)` | registered only while the sheet is open **and** topmost in its own group |
| `useSetBackdrop()` | suppress the manager's shared backdrop when the adapter draws its own |
| `useAdapterBackdrop(id, backdrop)` | applies the adapter's `backdrop?: BackdropConfig \| false` prop; two effects on purpose — value-sync (store bails on equal) and unmount-clear — so fresh JSX literals don't clear-and-rewrite every render |
| `useSetBackdrop()` | imperative form: `false` suppresses the shared backdrop (adapter draws its own), config restyles it, `true` clears |
| `useSheetPreventDismiss(id)` | whether an interceptor is blocking, so native gestures can be disabled |

**Drive `animatedIndex` continuously.** Setting it discretely in expand/close
Expand All @@ -250,7 +283,7 @@ when the show animation ends and `handleClosed` when the hide animation ends.

- Native `scrimColor` / `scrimOpacities` are gated on `modal` sheets on both
platforms. The manager always renders inline, so they can never paint — the
adapter does not accept them. Use `backdrop: false`.
adapter does not accept them. Use the `backdrop` prop (`false` to disable).
- `fullHeight` passes a detent taller than any screen and lets native clamp it.
Do **not** recompute `windowHeight - insets.top` in JS: since 0.16 there is no
JS-provided cap, and a JS estimate ignores that the sheet lives inside the
Expand Down Expand Up @@ -333,36 +366,6 @@ Consumer apps need nothing — Metro reads `exports` from package.json.

---

## File map

```
src/
├── index.tsx # public exports (no 3rd-party adapter deps)
├── testing.ts # → '…/testing'
├── store/ # store · hooks · helpers · types
├── bottomSheetCoordinator.ts # store ↔ adapter
├── refsMap · animatedRegistry · onBeforeCloseRegistry · portalSessionRegistry
├── adapter.types.ts · portal.types.ts
├── BottomSheetManager.provider.tsx / .context.tsx
├── BottomSheet.context.ts · BottomSheetRef.context.ts
├── BottomSheetDefaultIndex.context.ts # 0 for portal, -1 for persistent
├── BottomSheetHost · QueueItem · BottomSheetBackdrop · BottomSheetScaleView
├── BottomSheetPortal ('use no memo') · BottomSheetPersistent
├── useBottomSheetManager · useBottomSheetControl · useBottomSheetContext
├── useBottomSheetStatus · useOnBeforeClose · useSheetRenderData
├── useScaleAnimation · useStableCallback
├── useAdapterRef · useAnimatedIndex · useBackHandler
└── adapters/ # one directory per subpath export, no barrel
├── gorhom-sheet · custom-modal · react-native-modal · actions-sheet
└── swmansion/ # + SwmansionKeyboardInset (optional peer)
```

---

## Pitfalls

1. Do not memoize by hand — three sanctioned exceptions, listed above.
Expand All @@ -377,3 +380,8 @@ src/
8. Do not set `animatedIndex` discretely in an adapter.
9. Do not branch on `isOpen` for "is it on screen" — use `isVisible`.
10. Do not read `params` without `?.`.
11. Do not drop `setBackdrop`'s value-equality bail, and do not subscribe
`QueueItem` to the backdrop *config* — both turn one consumer render into a
store write that re-renders the whole sheet layer.
12. An adapter must never expose its library's own backdrop prop. The manager
renders the one backdrop; a second overlay stacks and is not stack-aware.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A stack manager for bottom sheets and modals in React Native. Supports `push`, `
- [Imperative vs Portal API](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/api-comparison)
- [Navigation Modes](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/navigation-modes)
- [Scale Animation](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/scale-animation)
- [Backdrop](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/backdrop)
- [Portal API (Context Preservation)](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/context-preservation)
- [Persistent Sheets](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/persistent-sheets)
- [Type-Safe IDs & Params](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/type-safe-ids)
Expand All @@ -21,6 +22,7 @@ A stack manager for bottom sheets and modals in React Native. Supports `push`, `
- **Adapter Architecture** - Pluggable adapters for different bottom sheet/modal libraries. Ships with adapters for `@gorhom/bottom-sheet`, `react-native-modal`, `react-native-actions-sheet`, `@swmansion/react-native-bottom-sheet`, and a custom modal. You can also build your own.
- **Stack Navigation** - `push`, `switch`, and `replace` modes for managing multiple sheets
- **Scale Animation** - iOS-style background scaling effect when sheets are stacked
- **Configurable Backdrop** - Theme the shared stack-aware backdrop per group or per sheet, or replace it with a custom component (e.g. blur)
- **Context Preservation** - Portal-based API that preserves React context in bottom sheets
- **Mixed Stacking** - Bottom sheets and modals coexist in the same stack
- **Persistent Sheets** - Pre-mounted sheets that open instantly and preserve state
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/adapters.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
---

# Library-Agnostic Architecture
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Root provider that manages the bottom sheet stack.
|------|------|----------|-------------|
| `id` | `string` | Yes | Unique identifier for this stack group |
| `scaleConfig` | `ScaleConfig` | No | Scale animation configuration |
| `backdrop` | `BackdropConfig \| false` | No | The group's default backdrop; `false` disables it for the whole group. A sheet overrides it with the `backdrop` prop on its adapter. See [Backdrop](/backdrop) |
| `children` | `ReactNode` | Yes | App content |

---
Expand Down
9 changes: 6 additions & 3 deletions docs/docs/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ is built from these. You do not need them to use the library.
| `useAdapterRef` | **Inside adapter only** | Resolve the right ref for inline/portal/persistent mode |
| `useAnimatedIndex` | **Inside adapter only** | The sheet's `animatedIndex` shared value, driving backdrop and scale |
| `useBackHandler` | **Inside adapter only** | Android back button, scoped to the topmost open sheet **of its own group** |
| `useSetBackdrop` | Anywhere | Returns `setBackdrop(id, boolean)` — suppress the manager's shared backdrop for a sheet that renders its own |
| `useAdapterBackdrop` | **Inside adapter only** | Applies the adapter's `backdrop` prop (`BackdropConfig \| false`) to the sheet — see [Backdrop](/backdrop) |
| `useSetBackdrop` | Anywhere | Returns `setBackdrop(id, value)` — `false` suppresses the manager's shared backdrop, a `BackdropConfig` restyles/replaces it, `true` clears the override |
| `useSheetPreventDismiss` | Anywhere | `useSheetPreventDismiss(id)` — whether an interceptor is currently blocking dismissal, so the adapter can disable native gestures |

---
Expand Down Expand Up @@ -109,9 +110,10 @@ open(<MySheet />, {
| `groupId` | `string` | context or `'default'` | Group ID for the sheet |
| `mode` | `OpenMode` | `'push'` | Navigation mode |
| `scaleBackground` | `boolean` | `false` | Enable background scaling |
| `backdrop` | `boolean` | `true` | When `false`, the manager's shared backdrop is not rendered for this sheet. `GorhomSheetAdapter` sets this itself when you pass it a custom `backdropComponent`; the other shipped adapters use the manager's backdrop and never touch it. |
| `params` | `Record<string, unknown>` | - | Params for the sheet, readable inside it via `useBottomSheetContext()`. Untyped here — the typed variant lives on `useBottomSheetControl` |

`backdrop?: boolean` was **removed in v3** — configure it on the adapter (the `backdrop` prop) or on the provider, not per `open()` call. See [Backdrop → Migration](/backdrop#migration-from-v2).

`open()` returns the sheet's ID, or **`null`** when the store declined to open it — because the sheet is already on the stack, or another sheet in the group is still animating open. A dev-mode warning explains which.

```tsx
Expand Down Expand Up @@ -258,9 +260,10 @@ open({
|--------|------|---------|-------------|
| `mode` | `OpenMode` | `'push'` | Navigation mode |
| `scaleBackground` | `boolean` | `false` | Enable background scaling |
| `backdrop` | `boolean` | `true` | When `false`, the manager's shared backdrop is not rendered for this sheet. `GorhomSheetAdapter` sets this itself when you pass it a custom `backdropComponent`; the other shipped adapters use the manager's backdrop and never touch it. |
| `params` | `BottomSheetPortalParams<T>` | - | Type-safe params |

`backdrop?: boolean` was **removed in v3** — configure it on the adapter (the `backdrop` prop) or on the provider, not per `open()` call. See [Backdrop → Migration](/backdrop#migration-from-v2).

`useBottomSheetManager().open()` also accepts `params` now, so inline sheets can read them from `useBottomSheetContext()` just like portal sheets.

---
Expand Down
36 changes: 36 additions & 0 deletions docs/docs/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,42 @@ const springConfig: ScaleAnimationConfig = {

---

### BackdropConfig

What the manager's shared backdrop renders — a discriminated union, `kind` first. Set as the group default via `backdrop` on the provider, or per sheet via the `backdrop` prop on an adapter (`false` there disables the backdrop). See [Backdrop](/backdrop).

```tsx
type BackdropConfig =
| {
kind: 'styled';
style?: StyleProp<ViewStyle>; // merged over the group's style and the default rgba(0,0,0,0.5)
pressToDismiss?: boolean; // default: true
}
| {
kind: 'custom';
component: ComponentType<BackdropComponentProps>;
pressToDismiss?: boolean; // default: true
};
```

---

### BackdropComponentProps

Props a `kind: 'custom'` backdrop component receives.

```tsx
type BackdropComponentProps = {
sheetId: string;
/** -1 hidden → 0 fully visible (see HIDDEN_ANIMATED_INDEX). */
animatedIndex: SharedValue<number>;
/** Calls requestClose(sheetId) — onBeforeClose interceptors still run. */
close: () => void;
};
```

---

## Portal Types

### BottomSheetPortalRegistry
Expand Down
Loading
Loading