In this hands-on lesson, we're going to create a simple module that uses native haptic feedback on iOS and Android. This is a fairly simple module, but it will lay down a strong foundation so you can keep building from here. Another goal is to spark your curiosity so you can continue experimenting with it and expanding it to be more flexible, depending on what you want to achieve.

We're going to learn how to create a module from scratch as a local module, which is the recommended approach. Then, once you're sure you want to publish it and make it available to everyone, you can extract it into a standalone package. For any new module you create, I recommend starting locally and using it in your app first, then deciding whether you want to open source it and maintain it.

Getting Started

To create a new Expo module, use the following command:

bunx create-expo-module@next cwb-haptics --local

For the latest stable version, use:

bunx create-expo-module cwb-haptics --local

When prompted, accept the defaults for module name and Android package name.

Project Structure

After creation, your module will have this structure:

modules/
  cwb-haptics/
    android/          # Kotlin implementation
    ios/              # Swift implementation
    src/
      index.ts        # Main exports
      CwbHaptics.types.ts  # TypeScript types
      CwbHapticsModule.ts  # Module definition
    expo-module.config.json

Key Files Explained

TypeScript Types

Define your module's API interface:

export type CwbHapticsModuleEvents = {};
 
export type HapticStyle = "light" | "medium" | "heavy";
 
export interface CwbHapticsModule {
  triggerHaptic(style: HapticStyle): Promise<void>;
}

iOS Implementation (Swift)

Uses UIImpactFeedbackGenerator for haptic feedback:

import ExpoModulesCore
import UIKit
 
public class CwbHapticsModule: Module {
    public func definition() -> ModuleDefinition {
        Name("CwbHaptics")
 
        AsyncFunction("triggerHaptic") { (style: String) in
            DispatchQueue.main.async {
                let generator: UIImpactFeedbackGenerator
 
                switch style.lowercased() {
                case "light":
                    generator = UIImpactFeedbackGenerator(style: .light)
                case "medium":
                    generator = UIImpactFeedbackGenerator(style: .medium)
                case "heavy":
                    generator = UIImpactFeedbackGenerator(style: .heavy)
                default:
                    generator = UIImpactFeedbackGenerator(style: .medium)
                }
 
                generator.prepare()
                generator.impactOccurred()
            }
        }
    }
}

Android Implementation (Kotlin)

Add the vibration permission to modules/cwb-haptics/android/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.VIBRATE" />
</manifest>

Then implement the vibration logic with waveform patterns:

package expo.modules.cwbhaptics
 
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
 
class CwbHapticsModule : Module() {
    private val context: Context
        get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
 
    private val vibrator: Vibrator
        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            (context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager).defaultVibrator
        } else {
            @Suppress("DEPRECATION")
            context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
        }
 
    override fun definition() = ModuleDefinition {
        Name("CwbHaptics")
 
        AsyncFunction("triggerHaptic") { style: String ->
            val (timings, amplitudes, oldPattern) = when (style.lowercase()) {
                "light" -> Triple(longArrayOf(0, 10), intArrayOf(0, 50), longArrayOf(0, 10))
                "heavy" -> Triple(longArrayOf(0, 50), intArrayOf(0, 255), longArrayOf(0, 50))
                else -> Triple(longArrayOf(0, 20), intArrayOf(0, 128), longArrayOf(0, 20)) // medium
            }
 
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, -1))
            } else {
                @Suppress("DEPRECATION")
                vibrator.vibrate(oldPattern, -1)
            }
 
        }
    }
}

Usage in Your App

Import and use your module:

import { Button, StyleSheet, View } from "react-native";
import CwbHaptics from "../../modules/cwb-haptics";
 
export default function Index() {
  return (
    <View style={styles.container}>
      <Button
        title="Light Haptic"
        onPress={async () => {
          await CwbHaptics.triggerHaptic("light");
        }}
      />
      <Button
        title="Medium Haptic"
        onPress={async () => {
          await CwbHaptics.triggerHaptic("medium");
        }}
      />
      <Button
        title="Heavy Haptic"
        onPress={async () => {
          await CwbHaptics.triggerHaptic("heavy");
        }}
      />
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
});

Development Tips

  • Use Xcode for Swift code - Better syntax highlighting and autocomplete
  • Use Android Studio for Kotlin code - Better development experience
  • Recompile after native changes - Native code changes require full rebuild
  • Test on real devices - Haptic feedback doesn't work well in simulators
  • Keep modules focused - One module should do one thing well

Building and Testing

iOS

npx expo prebuild --platform ios
xed ios

Then build and run from Xcode (⌘R).

Android

npx expo prebuild --platform android
# Open android folder in Android Studio

Or use:

open -a "Android Studio" android

Best Practices

  1. Start with local modules before publishing
  2. Keep modules isolated and focused on one responsibility
  3. Test thoroughly on both platforms and real devices
  4. Document your module's API clearly
  5. Consider edge cases and API version compatibility

Resources

Is this lesson useful?