Expo Dynamic Notifications: Dynamic Island-Style Alerts for React Native

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 render function.
  • 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 ios

Mount 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

APIPurpose
<DynamicNotifications duration={3600}>Sets the default auto-dismiss delay in milliseconds; null disables it.
strength, blur, gain, thresholdAdjust the Skia goo effect. blur overrides the blur derived from strength.
islandWidth, islandHeight, islandTop, gapPosition and size the island relative to the card.
cardWidth, cardHeight, cardRadiusSet card geometry.
islandColor, cardColor, shadowColor, accentSet default colors for the island, card, shadow, and title or symbol.
onDismiss(notification)Run after the notification’s exit animation.
styleStyle 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, layoutRead visibility, current content, and resolved geometry.

Notification fields

FieldPurpose
titleRequired first line of the default body.
messageOptional second line.
avatarImage URI on the left of the default body.
symbolIcon name used by the default body.
accentOverride the root tint for this alert.
durationOverride the root lifetime; null disables auto-dismiss.
onPressRun on tap before dismissal.
render(notification)Replace the default body with custom React Native content.
idOptional identifier returned through onDismiss.

Alternatives and Related Resources

Add Comment