Description:
expo-dynamic-notifications is an Expo Router source template for Dynamic Island-style in-app alerts.
A Skia island expands into a card above the app, and screens can trigger custom content, handle taps, or dismiss the alert through a shared hook.
Features
- A morphing island, connecting droplet, and notification card.
- App-wide overlay mounted once around the router.
- Swipe dismissal, tap callbacks, and a configurable auto-dismiss timer.
- Custom card content through the notification’s
renderfunction. - Root-level colors, island geometry, card dimensions, and goo controls.
trigger,dismiss, and visibility state.
How To Use It
Run the template
Clone the app and install its dependencies. On macOS, bun ios builds the native app and generates the iOS project when needed. For Android, replace the last command with bun android:
git clone https://github.com/rit3zh/expo-dynamic-notifications
cd expo-dynamic-notifications
bun install
bun iosMount the overlay
Place DynamicNotifications inside both GestureHandlerRootView and SafeAreaProvider. In the template, src/app/_layout.tsx mounts it around the Expo Router stack:
import { DynamicNotifications } from "@/components";
import { Stack } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<DynamicNotifications duration={2500}>
<Stack screenOptions={{ headerShown: false }} />
</DynamicNotifications>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}Trigger an alert
Call the hook below the root wrapper. title is required; message, avatar, symbol, accent, duration, onPress, render, and id are optional notification fields.
import { useDynamicNotifications } from "@/hooks";
import { Button } from "react-native";
export function SaveButton() {
const { trigger } = useDynamicNotifications();
return (
<Button
title="Save"
onPress={() => trigger({
title: "Saved",
message: "Your changes are ready.",
accent: "#2F9BFF",
})}
/>
);
}Duration and Custom Content
The root’s duration sets the default lifetime in milliseconds. A notification can override it; null keeps that alert open until a tap, swipe, or dismiss() call.
If an alert is visible, trigger() starts its exit and shows the newest queued alert when that exit finishes.
const { trigger, dismiss } = useDynamicNotifications();
trigger({ title: "Uploading", duration: null });
// Call this when the upload ends or is canceled.
dismiss();Key API
| API | Purpose |
|---|---|
<DynamicNotifications duration={3600}> | Sets the default auto-dismiss delay in milliseconds; null disables it. |
strength, blur, gain, threshold | Adjust the Skia goo effect. blur overrides the blur derived from strength. |
islandWidth, islandHeight, islandTop, gap | Position and size the island relative to the card. |
cardWidth, cardHeight, cardRadius | Set card geometry. |
islandColor, cardColor, shadowColor, accent | Set default colors for the island, card, shadow, and title or symbol. |
onDismiss(notification) | Run after the notification’s exit animation. |
style | Style the root container. |
trigger(notification) | Start the visible alert’s exit, then show the newest queued notification. |
dismiss() | Close the current notification and clear the pending alert. |
isVisible, notification, layout | Read visibility, current content, and resolved geometry. |
Notification fields
| Field | Purpose |
|---|---|
title | Required first line of the default body. |
message | Optional second line. |
avatar | Image URI on the left of the default body. |
symbol | Icon name used by the default body. |
accent | Override the root tint for this alert. |
duration | Override the root lifetime; null disables auto-dismiss. |
onPress | Run on tap before dismissal. |
render(notification) | Replace the default body with custom React Native content. |
id | Optional identifier returned through onDismiss. |