diff --git a/.gitignore b/.gitignore
index 2ba1fc0..7efecc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,4 @@ local.properties
# Packages
*.tgz
+.DS_Store
diff --git a/README.md b/README.md
index f05ef4c..35823ea 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,30 @@ for the following:
- [React Native](https://reactnative.dev/) >= 0.50.0
- [react-native-svg](https://github.com/react-native-svg/react-native-svg) >= 12.1.0
+## ✨ Edge-to-Edge Support
+
+This library supports edge-to-edge display (including Android 15’s enforced mode). The tour overlay already covers the whole screen, system bars included.
+
+When your screens use a translucent status bar you can enable proper handling—and optionally nudge the spotlight—via the new `translucentStatusBar` prop:
+
+```tsx
+
+ {/* … */}
+
+```
+
+`insets.top` is the height of the status bar returned by `react-native-safe-area-context`. Any value that represents the actual status-bar height will work.
+
+`translucentStatusBar` is **optional**—if you omit it or set `enable` to `false`, the tour continues to behave exactly as before.
+
+See the [example app](example/) for a fully-working edge-to-edge implementation.
+
## Install
With `npm`:
diff --git a/example/android/app/src/main/java/com/spotlightexample/MainActivity.kt b/example/android/app/src/main/java/com/spotlightexample/MainActivity.kt
index 824e98f..9271be6 100644
--- a/example/android/app/src/main/java/com/spotlightexample/MainActivity.kt
+++ b/example/android/app/src/main/java/com/spotlightexample/MainActivity.kt
@@ -1,5 +1,10 @@
package com.spotlightexample
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.WindowInsetsController
+
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
@@ -7,6 +12,28 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(null)
+
+ val window = window
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Edge-to-edge for Android 11+
+ window.setDecorFitsSystemWindows(false)
+ val controller = window.insetsController
+ controller?.systemBarsBehavior =
+ WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
+ } else {
+ // Edge-to-edge for older versions
+ @Suppress("DEPRECATION")
+ window.decorView.systemUiVisibility = (
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ )
+ }
+ }
+
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 0787479..67346ce 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1648,6 +1648,96 @@ PODS:
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- SocketRocket
+ - react-native-safe-area-context (5.6.1):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - react-native-safe-area-context/common (= 5.6.1)
+ - react-native-safe-area-context/fabric (= 5.6.1)
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
+ - react-native-safe-area-context/common (5.6.1):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
+ - react-native-safe-area-context/fabric (5.6.1):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - react-native-safe-area-context/common
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- React-NativeModulesApple (0.80.0):
- boost
- DoubleConversion
@@ -2226,6 +2316,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
+ - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -2347,6 +2438,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
React-microtasksnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
+ react-native-safe-area-context:
+ :path: "../node_modules/react-native-safe-area-context"
React-NativeModulesApple:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
React-oscompat:
@@ -2455,6 +2548,7 @@ SPEC CHECKSUMS:
React-logger: b69e65dc60f768e5509ac0cc27a360124bf70478
React-Mapbuffer: b48f9f3311fd0ec0f7a5dc39d707eff521fb5f38
React-microtasksnativemodule: d8568d0485a350c720c061ae835e09fc88c28715
+ react-native-safe-area-context: fd72bd1eb562ff2a0fda20e9a7828c4b0cabf5a4
React-NativeModulesApple: f10596688a03af66804cfbe61792be24a7888da8
React-oscompat: 7c0a341cc31e350da71ddf2e46de0a845d1d1626
React-perflogger: 4cc44451f694d6205f47bd8d5d87c9c862c3335c
@@ -2488,8 +2582,8 @@ SPEC CHECKSUMS:
ReactCommon: 658874decaf8c4fd76cfa3a878b94a869db85b1c
RNSVG: c73af7848d94ca3e8136a5191d055e3c1d6fedab
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- Yoga: 395b5d614cd7cbbfd76b05d01bd67230a6ad004e
+ Yoga: 0c4b7d2aacc910a1f702694fa86be830386f4ceb
PODFILE CHECKSUM: 9742088751b8c39a438474c638da6274a8066e53
-COCOAPODS: 1.14.3
+COCOAPODS: 1.15.2
diff --git a/example/package.json b/example/package.json
index 2bbb898..e36ed41 100644
--- a/example/package.json
+++ b/example/package.json
@@ -26,6 +26,7 @@
"react-dom": "19.1.0",
"react-is": "19.1.0",
"react-native": "0.80.0",
+ "react-native-safe-area-context": "^5.6.1",
"react-native-spotlight-tour": "workspace:^",
"react-native-svg": "^15.12.0",
"react-native-svg-web": "^1.0.9",
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 465cea1..42eb1b5 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,8 +1,11 @@
import dedent from "dedent";
import { type ReactElement, useCallback, useMemo } from "react";
-import { Alert, Animated, Button, Dimensions, SafeAreaView, Text, useAnimatedValue } from "react-native";
+import { Alert, Animated, Button, Dimensions, Text, useAnimatedValue } from "react-native";
+import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import {
AttachStep,
+ type RenderProps,
+ type SpotlightTour,
SpotlightTourProvider,
TourBox,
type TourState,
@@ -19,9 +22,9 @@ import {
} from "./App.styles";
import { DocsTooltip } from "./DocsTooltip";
-export function App(): ReactElement {
+function AppContent(): ReactElement {
const gap = useAnimatedValue(0, { useNativeDriver: true });
-
+ const { top } = useSafeAreaInsets();
const showSummary = useCallback(({ index, isLast }: TourState) => {
Alert.alert(
"Tour Finished",
@@ -40,7 +43,7 @@ export function App(): ReactElement {
}, []);
const tourSteps = useMemo((): TourStep[] => [{
- render: ({ next, pause }) => (
+ render: ({ next, pause }: RenderProps) => (
{"Tour: Intro section\n"}
@@ -59,7 +62,7 @@ export function App(): ReactElement {
render: DocsTooltip,
}, {
arrow: true,
- render: props => (
+ render: (props: RenderProps) => (
resolve());
});
},
- render: ({ previous, stop }) => (
+ flip: true,
+ placement: "top",
+ render: ({ previous, stop }: RenderProps) => (
{"Tour: Try it!\n"}
@@ -115,11 +120,11 @@ export function App(): ReactElement {
}], []);
return (
-
+
- {({ resume, start, status }) => (
+ {({ resume, start, status }: SpotlightTour) => (
<>
{status !== "paused" && }
{status === "paused" && }
-
- {"Introduction"}
+
+ {"Introduction."}
{dedent`
@@ -146,7 +155,7 @@ export function App(): ReactElement {
-
+
{"Documentation"}
@@ -179,3 +188,11 @@ export function App(): ReactElement {
);
}
+
+export function App(): ReactElement {
+ return (
+
+
+
+ );
+}
diff --git a/example/src/DocsTooltip.tsx b/example/src/DocsTooltip.tsx
index 0080a3a..33fb353 100644
--- a/example/src/DocsTooltip.tsx
+++ b/example/src/DocsTooltip.tsx
@@ -1,6 +1,6 @@
import dedent from "dedent";
import { Button } from "react-native";
-import { useSpotlightTour } from "react-native-spotlight-tour";
+import { type SpotlightTour, useSpotlightTour } from "react-native-spotlight-tour";
import { BoldText, ButtonsGroupView, DescriptionText, SpotDescriptionView } from "./App.styles";
@@ -8,7 +8,7 @@ import type { ReactElement } from "react";
export function DocsTooltip(): ReactElement {
// You can also use the hook instead of the props here!
- const { next, previous } = useSpotlightTour();
+ const { next, previous }: SpotlightTour = useSpotlightTour();
return (
diff --git a/package/src/lib/SpotlightTour.context.ts b/package/src/lib/SpotlightTour.context.ts
index 4c247b6..33ca340 100644
--- a/package/src/lib/SpotlightTour.context.ts
+++ b/package/src/lib/SpotlightTour.context.ts
@@ -177,6 +177,11 @@ export interface TooltipProps {
* @default { padding: 8 }
*/
shift?: boolean | ShiftOptions;
+ /**
+ * Global coordinate adjustment for all spotlight calculations.
+ * Useful for edge-to-edge layouts or custom status bar configurations.
+ */
+ translucentStatusBar?: TranslucentStatusBar;
}
export interface TourStep extends TooltipProps {
@@ -279,6 +284,10 @@ export interface SpotlightTourCtx extends SpotlightTour {
* The list of steps for the tour.
*/
steps: TourStep[];
+ /**
+ * Global coordinate adjustment for all spotlight calculations.
+ */
+ translucentStatusBar?: TranslucentStatusBar;
}
export const ZERO_SPOT: LayoutRectangle = {
@@ -300,6 +309,7 @@ export const SpotlightTourContext = createContext({
status: "idle",
steps: [],
stop: () => undefined,
+ translucentStatusBar: undefined,
});
/**
@@ -322,3 +332,24 @@ export function useSpotlightTour(): SpotlightTour {
stop,
};
}
+export interface CoordinateAdjustment {
+ /**
+ * Global X offset to apply to all spotlight calculations
+ */
+ x?: number;
+ /**
+ * Global Y offset to apply to all spotlight calculations
+ */
+ y?: number;
+}
+export interface TranslucentStatusBar {
+ /**
+ * Global coordinate adjustment for all spotlight calculations.
+ */
+ coordinateAdjustment?: CoordinateAdjustment;
+ /**
+ * Whether to enable the translucent status bar
+ */
+ enable?: boolean;
+
+}
diff --git a/package/src/lib/SpotlightTour.provider.tsx b/package/src/lib/SpotlightTour.provider.tsx
index 6ea53bc..805efec 100644
--- a/package/src/lib/SpotlightTour.provider.tsx
+++ b/package/src/lib/SpotlightTour.provider.tsx
@@ -130,6 +130,7 @@ export const SpotlightTourProvider = forwardRef();
@@ -246,7 +247,8 @@ export const SpotlightTourProvider = forwardRef ({
current,
@@ -268,6 +270,7 @@ export const SpotlightTourProvider = forwardRef(null);
@@ -77,10 +78,18 @@ export function AttachStep({ children, fill = false, index, style }: AttachStepP
if (current !== undefined && indexes.includes(current)) {
ref.current?.measureInWindow((x, y, width, height) => {
- changeSpot({ height, width, x, y });
+ if (!translucentStatusBar?.enable || Platform.OS === "ios") {
+ changeSpot({ height, width, x, y });
+ return;
+ }
+ const { coordinateAdjustment } = translucentStatusBar;
+ const globalX = coordinateAdjustment?.x || 0;
+ const globalY = coordinateAdjustment?.y || 0;
+
+ changeSpot({ height, width, x: x + globalX, y: y + globalY });
});
}
- }, [changeSpot, current, JSON.stringify(index)]);
+ }, [changeSpot, current, JSON.stringify(index), translucentStatusBar]);
const onLayout = useCallback((event: LayoutChangeEvent): void => {
updateSpot();
diff --git a/package/src/lib/components/tour-overlay/TourOverlay.component.tsx b/package/src/lib/components/tour-overlay/TourOverlay.component.tsx
index 127c1d6..3652c9f 100644
--- a/package/src/lib/components/tour-overlay/TourOverlay.component.tsx
+++ b/package/src/lib/components/tour-overlay/TourOverlay.component.tsx
@@ -79,7 +79,9 @@ export const TourOverlay = forwardRef((props,
...tooltipProps
} = props;
- const { goTo, next, pause, previous, resume, start, steps, stop } = useContext(SpotlightTourContext);
+ const {
+ goTo, next, pause, previous, resume, start, steps, stop, translucentStatusBar,
+ } = useContext(SpotlightTourContext);
const arrowRef = useRef(null);
@@ -187,6 +189,7 @@ export const TourOverlay = forwardRef((props,
supportedOrientations={["portrait", "landscape", "landscape-left", "landscape-right", "portrait-upside-down"]}
transparent={true}
visible={current !== undefined}
+ statusBarTranslucent={translucentStatusBar?.enable ?? false}
>