feat!: customizable backdrop (color, style, or custom component) - #46
Merged
Conversation
Implements #45. The shared stack-aware backdrop keeps its behavior (mount timing, animatedIndex-driven fade, z-index handling, requestClose on tap) but what it renders is now configurable at two levels, most specific winning: - Group default: `backdropConfig` on `BottomSheetManagerProvider`. - Per sheet: a `backdrop?: BackdropConfig | false` prop on every shipped adapter, applied via the new public `useAdapterBackdrop` hook so third-party adapters reach parity. `BackdropConfig` is a discriminated union (`kind: 'styled' | 'custom'`), mirroring `OpenPayload` — styled configs merge styles over the group and default, custom components receive `{ sheetId, animatedIndex, close }` and own their fade; `pressToDismiss` resolves per field. `setBackdrop` widens compatibly to `boolean | BackdropConfig` (`true` clears the override) and bails on value-equal writes so fresh JSX object literals don't wake store subscribers every render. BREAKING CHANGE: `backdrop?: boolean` is removed from `open()` options on `useBottomSheetManager` and `useBottomSheetControl`. Pass `backdrop={false}` (or a config) to the sheet's adapter instead — it works identically across inline, portal, and persistent modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TY6uDERCua6Z4UrwU1wUjk
Review follow-ups on the configurable-backdrop work.
- GorhomSheetAdapter no longer accepts gorhom's `backdropComponent`: it is
omitted from the props type and forced to render nothing. The manager
draws the one shared, stack-aware backdrop for every sheet, so the
adapter now matches the other four. This removes the inference +
precedence + dev-warning branch entirely — and with it a warning that
wrongly claimed stacking for the sound `backdrop={false}` combination.
- QueueItem subscribes to a new `useSheetBackdropEnabled` boolean instead
of the config object, so restyling no longer re-renders the memoized
sheet layer. Only BottomSheetBackdrop reads the config.
- `backdropValuesEqual` moves out of `store/helpers.ts` into
`backdrop.equality.ts`, keeping helpers pure stack operations and the
store layer free of React Native imports.
- The reanimated mock gains a passthrough `Animated.View` in jest.setup.ts,
dropping a ~40-line near-verbatim re-mock from backdrop.test.tsx.
- Docs: gorhom backdrop section rewritten, `backdropComponent` migration
documented, duplicate `sidebar_position: 6` resolved, CLAUDE.md file map
and pitfalls updated.
BREAKING CHANGE: `GorhomSheetAdapter` no longer accepts `backdropComponent`.
Use `backdrop={{ kind: 'custom', component: MyBackdrop }}` instead — it
renders in the manager's stack-aware backdrop layer and receives
`{ sheetId, animatedIndex, close }`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md carries project guidelines, not an inventory of files. The map duplicated the directory tree, said nothing a reader could act on, and went stale on every added or renamed file. The guidance that happens to name a symbol stays — those entries encode why a piece of code is the way it is (the sanctioned memoization exceptions, the registry table, the pitfalls), which is the point of the document. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t review
Four review passes (API design, correctness, readability, docs accuracy)
against the configurable-backdrop work. Every fix below carries a test that
fails without it.
Correctness:
- `backdropValuesEqual` walked only one style's keys and used the key count
as a proxy for the key set, so a style carrying an explicit `undefined`
compared equal to one carrying a real value in its place — `setBackdrop`
bailed and the restyle was silently dropped. Comparing flattened JSON fixes
it and cuts the file roughly in half; key-order sensitivity is the only
cost, and it buys one redundant write, never a wrong render.
- A config style's own `opacity` beat the manager's fade, because
`animatedStyle` sat before the user styles in the array. It now sits last.
- `useAdapterBackdrop` applies the prop in a layout effect. The write lands a
commit after the backdrop first renders; for a `styled` group default that
frame is invisible (opacity 0), but a `custom` one owns its fade and painted
at full strength before the sheet's own config replaced it.
API:
- The provider's `backdropConfig?: BackdropConfig` becomes
`backdrop?: BackdropConfig | false` — same name and type as the adapter
prop, and a group can now opt out of backdrops entirely. `QueueItem` reads a
stable `'off' | 'own' | 'inherit'` selector and folds in the group value, so
it still never subscribes to the config object.
- Adapters share `AdapterBackdropProps` instead of five copies of the same
prop and JSDoc.
- Resolution moves to `backdrop.resolve.ts`, which makes the atomic-visual /
per-field-pressToDismiss rule unit-testable and drops the `let` in
`BottomSheetBackdrop`.
Tests: cover the two decisions that had none — that a value-equal fresh
literal produces no store write (merging the two effects passes otherwise),
and that restyling does not re-render the memoized sheet layer.
Docs: the blur example was missing its `StyleSheet` import; `custom-adapters`
still taught the hand-rolled effect instead of `useAdapterBackdrop`; three
adapter pages never documented their new prop; `backdrop.md` still referenced
gorhom's removed `backdropComponent`; `intro` lacked the feature README has.
Also documents that `backdrop={false}` removes the touch shield that
`pressToDismiss: false` keeps, and how to vary a backdrop per open via params.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The example only ever used `backdrop={false}`, so the new configuration
surface was never run — which is also how the JSX-text bug below survived
review.
Adds a Backdrop demo that stacks all four variants so the resolution rules
are visible rather than described: the default scrim, a `kind: 'custom'`
expo-blur backdrop, a `kind: 'styled'` tint, and `pressToDismiss: false`.
`BlurBackdrop` is declared at module scope on purpose — the config is
compared by component identity, so an inline arrow would be a new type every
render and would remount the backdrop, restarting the blur.
Verified on an iPhone 17 Pro Max simulator: the blur intensity follows
`animatedIndex` continuously (weakens mid-drag, recovers on snap-back)
rather than switching discretely, tap-to-dismiss still routes through
`requestClose` with a custom component in place, the tint layers over the
sheet below while leaving the top sheet untouched, and `pressToDismiss:
false` keeps the sheet open while still blocking touches.
Also fixes a JSX text node where `{false}` was parsed as an expression and
rendered nothing, so the copy read "backdrop=" instead of "backdrop={false}".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #45.
custom-backdrop.mov
The shared backdrop's appearance was hardcoded to
rgba(0, 0, 0, 0.5), and the only knob anywhere in the public API wasbackdrop: falseonopen(), which turned it off entirely. Theming, blur, and per-sheet variation all required either patching the package or rendering your own backdrop inside the sheet — which loses everything the shared one provides: stack-aware z-indexing, living outside theBottomSheetScaleViewtransform, and being driven from the sheet's liveanimatedIndex.This makes what the backdrop renders configurable while leaving all of that behaviour with the manager.
API
Two levels, the sheet's choice winning over the group's:
A discriminated union rather than three mutually-exclusive optional fields, mirroring
OpenPayload—kindis what callers reason about. A custom component receives the rawanimatedIndexrather than a pre-computed opacity, so blur intensity or a gradient can be driven from the sheet's real position on the UI thread; it therefore owns its own fade, and the built-in opacity is deliberately not applied on top.Resolution is atomic for the visual choice — a sheet-level config replaces the group's rendering entirely, so a group
customcomponent never bleeds under a sheet that asked for a styled scrim. Styles compose (default → group → sheet) where both levels arestyled, andpressToDismissresolves per field.Adapter authors get
useAdapterBackdrop(id, backdrop)andAdapterBackdropProps, which is exactly what the five shipped adapters use.Breaking changes
1.
backdrop?: booleanremoved fromopen()options (useBottomSheetManager,useBottomSheetControl).Declaring it on the adapter works identically across all three operating modes, and keeping
open()out of the field is what lets a persistent sheet's backdrop survive close/re-open cycles. The capability that genuinely moves is per-open variation — the migration guide shows theparamsroute.2.
backdropComponentremoved fromGorhomSheetAdapterProps. The manager always renders the backdrop, so the two can never stack. Passing gorhom's own silently disabled the stack-aware one and substituted a per-sheet backdrop that does not participate in the stack — a bug shaped like a feature.backdrop={{ kind: 'custom', component }}replaces it.Both are documented under Backdrop → Migration. This lands as 3.0.0.
Testing
117 tests pass (24 new), plus typecheck, lint, and a docs build with
onBrokenLinks: 'throw'.Per CLAUDE.md's "write the test so it fails without the fix", each new test was confirmed to fail against the unfixed code. The ones worth calling out cover decisions that would otherwise silently regress:
useAdapterBackdrop's two effects into the obvious one-effect-with-cleanup form passes every other testQueueItemsubscribes to a stable'off' | 'own' | 'inherit'primitive, never the config object)opacitydoes not replace the manager's fadeundefinedis not mistaken for one carrying a real value in its placerequestClose, so a refusingonBeforeCloseinterceptor keeps the sheet openReview notes
The implementation was reviewed by four independent agents (API design, correctness, readability/DRY, docs-vs-code accuracy). Findings that survived verification are folded into the commits above — the substantive ones were the equality false-positive, the
opacityordering, a one-frame flash of a group-levelcustombackdrop (now applied in a layout effect), and the two untested design decisions listed above.🤖 Generated with Claude Code