react-native-alarm-scheduler
Guides

Custom alarm sounds

Let users choose a different local audio file for each alarm.

Silent alarms

Set silent: true in either platform’s options when the alarm should present normally without playing audio:

await AlarmScheduler.scheduleAlarmAsync({
  hour: 7,
  minute: 0,
  ios: { silent: true },
  android: { silent: true, vibrate: true },
});

silent takes precedence over soundUri and soundName. On Android it skips audio playback and alarm-volume enforcement; vibrate remains independent, so silent: true, vibrate: true is a vibrate-only alarm. The notification and full-screen ringing UI still appear.

On a physical iOS 26+ device, the package uses its own bundled silent CAF while AlarmKit continues to own the alert presentation and vibration. The config plugin adds this asset automatically. If the asset is missing, scheduling fails instead of falling back to an audible system sound.

Silent AlarmKit alarms require a physical device

The iOS Simulator cannot safely use named AlarmKit sounds, including the package’s silent sound. Silent scheduling is therefore rejected in the Simulator rather than becoming unexpectedly audible.

User-selected audio

Use any document or media picker that returns a readable local URI. With Expo, install the SDK-matched document picker:

npx expo install expo-document-picker

Then pass the selected URI directly to the alarm:

import * as DocumentPicker from 'expo-document-picker';
import AlarmScheduler from 'react-native-alarm-scheduler';

const result = await DocumentPicker.getDocumentAsync({
  type: 'audio/*',
  copyToCacheDirectory: true,
});

if (!result.canceled) {
  await AlarmScheduler.scheduleAlarmAsync({
    hour: 7,
    minute: 0,
    soundUri: result.assets[0].uri,
  });
}

soundUri is common to both platforms. You may instead set ios.soundUri or android.soundUri when the platforms should use different files. Each alarm can use a different URI.

The scheduler imports the audio while scheduleAlarmAsync runs, so the alarm does not depend on a temporary picker permission or cache file later:

  • iOS: converts the first 29 seconds to a PCM CAF file in the app’s Library/Sounds directory, which AlarmKit can resolve while the app is not running.
  • Android: copies the original into the app’s private files directory and loops it through the native alarm audio stream.

The imported copy is replaced when the same alarm id is rescheduled and removed when the alarm is cancelled. The source file may be moved or deleted after scheduling.

Readable files only

DRM-protected Apple Music, Spotify and similar subscription tracks are not readable local audio files and cannot be imported. Exported or downloaded audio files from Files, Downloads, cloud-drive providers and media-creation apps work when the picker grants access.

Why iOS uses the first 29 seconds

AlarmKit uses the system alert-sound facility. Apple requires alert sounds to be under 30 seconds and stored in the app bundle or Library/Sounds. The module performs the conversion and trim so a normal user-selected MP3, M4A, WAV, AIFF or CAF file can be used without a build-time config change. See Apple’s AlarmKit custom-sound guidance.

iOS Simulator uses the system default

iOS 26.x Simulator can crash SpringBoard inside Apple’s private ToneLibrary when AlarmKit starts an external sound. To keep development builds stable, the package uses the system default sound in the Simulator and reports soundFallbackReason: 'iosSimulatorCustomSoundUnsupported' in native debug state. Test custom sound playback on a physical iOS 26+ device. Android emulators are unaffected.

Android fallback behavior

The foreground ring service plays the selected file. If Android refuses to start that service and the package must fall back to a system notification channel, it uses the system alarm tone because the notification service cannot read the app’s private imported file.

Build-time bundled sounds

Bundled sounds remain useful when the developer owns the fixed sound catalog. On iOS, add compatible sound files to the config plugin:

app.json
{
  "expo": {
    "plugins": [
      [
        "react-native-alarm-scheduler",
        { "iosAlarmSounds": ["./assets/audio/morning-alarm.caf"] }
      ]
    ]
  }
}

Reference the exact filename:

await AlarmScheduler.scheduleAlarmAsync({
  hour: 7,
  minute: 0,
  ios: { soundName: 'morning-alarm.caf' },
});

On Android, place the file in android/app/src/main/res/raw and use its resource name:

await AlarmScheduler.scheduleAlarmAsync({
  hour: 7,
  minute: 0,
  android: { soundName: 'morning_alarm' },
});

For audible alarms, runtime soundUri takes precedence over bundled soundName; silent takes precedence over both.

Staying audible on Android

Playback loops on STREAM_ALARM, which ignores the ringer and Do Not Disturb. On top of that, the package defends against the volume-down escape hatch:

await AlarmScheduler.scheduleAlarmAsync({
  hour: 7,
  minute: 0,
  android: {
    enforceVolume: true,
    restoreVolume: true,
    volume: 1,
    vibrate: true,
  },
});

With enforceVolume on, the alarm stream is pinned at volume for the duration of the ring and re-raised whenever anything lowers it. The user’s original level is restored afterwards unless restoreVolume is false.

iOS alarm volume is owned by AlarmKit and is not configurable from the package.