Android overlay module / Expo + React Native

Overlay permission, floating bubble UI, and shared state for Expo Android apps.

Use the module to request draw-over-apps permission, launch a movable floating bubble, keep app and bubble state in sync, and swap in your own renderer when the default UI is not enough.

Android only

Focused on overlays, permissions, and service-driven UI instead of pretending to be a broad cross-platform abstraction.

React Native bubble

Use the shipped floating UI or replace it with your own renderer and design language.

Open repo workflow

Docs, source, the example app, and issues all stay connected to the same public GitHub project.

Live overlay preview

Permission, service, and UI in one surface

State synced

In-app controls

Overlay permission

Granted

Ready

Bubble

Visible

Shared count

14

Hide bubble+1 in app

Install

npm install expo-draw-over-apps

Use a dev build, custom client, or standalone Android build. Expo Go is not supported.

Event stream

requestPermission() opened settings
App resumed and permission refreshed
showBubble() accepted by the native module

What ships

One Android overlay module that covers permission flow, floating UI, and shared bubble state.

expo-draw-over-apps gives you the permission helpers, overlay service controls, renderer customization hook, and synchronized state helpers you need to ship a floating bubble in an Expo app.

Permission flow

Ask for overlay access only when Android actually blocks you.

Start with canDrawOverlays(), send users into settings only when needed, then refresh when the app becomes active again.

canDrawOverlays()requestPermission()Lifecycle friendly

Renderer swap

Keep the native service and still design the bubble your way.

setBubbleRenderer() gives you a narrow seam for customization without throwing away the working Android overlay plumbing.

Custom UIReact Native componentsStyleSheet or NativeWind

Realtime sync

The bubble and the app stay aligned on count, visibility, and source of change.

useBubbleState(), refreshBubbleState(), and the counter helpers make the overlay feel like part of the app instead of a detached widget.

useBubbleState()Shared counterVisible state

Expo fit

Made for dev builds, custom clients, and standalone Android builds.

This module ships native Android code, so the docs stay honest about where it runs and where it does not.

No Expo GoDevelopment buildsAndroid service

Open source shape

Readers can move from the docs into code, issues, and the example app in one hop.

That keeps the website useful without becoming a documentation island that drifts away from the repository.

GitHubExample appIssues

How it works

The integration path stays short from Android permission to a live floating bubble.

Most apps will follow the same flow: check permission, hand users off to settings when needed, show the bubble service, then keep state synchronized between the app and the overlay.

01

Read permission state

Use canDrawOverlays() to know whether Android already allows the overlay window.

02

Send the user to settings

Call requestPermission() only when permission is missing, then wait for the app to become active again.

03

Start the bubble service

showBubble() makes the floating UI visible and ready to move above other apps.

04

Sync app and bubble actions

Counter updates and visibility changes stay in sync through the shared state helpers.

Quick start

Start with the default bubble, then move into custom or Tailwind-powered renderers.

The examples below cover the common module paths: a permission-first setup, a custom renderer with plain React Native styles, and a NativeWind or Tailwind CSS renderer when your app already uses utility classes.

Install

npm install expo-draw-over-apps

Use a development build, custom client, or standalone Android build.

Expo Go is not supported because the package contains native Android code.

No extra manual manifest setup should be required in a normal Expo modules integration.

Deployment notes

Android only

Overlay permission must be granted by the user

The floating bubble is hosted by an Android service

Custom bubble UI still uses React Native components

Basic usage

tsx
import { useEffect, useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import {
  canDrawOverlays,
  hideBubble,
  incrementBubbleCount,
  requestPermission,
  showBubble,
  useBubbleState,
} from 'expo-draw-over-apps';

export default function App() {
  const [granted, setGranted] = useState(false);
  const bubbleState = useBubbleState();

  useEffect(() => {
    setGranted(canDrawOverlays());
  }, []);

  async function handlePermission() {
    await requestPermission();
    setGranted(canDrawOverlays());
  }

  async function handleToggleBubble() {
    if (bubbleState.isVisible) {
      hideBubble();
      return;
    }

    await showBubble();
  }

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }}>
      <Text>Overlay permission: {granted ? 'granted' : 'missing'}</Text>
      <Text>Bubble visible: {bubbleState.isVisible ? 'yes' : 'no'}</Text>
      <Text>Counter: {bubbleState.count}</Text>

      <Pressable onPress={() => void handlePermission()}>
        <Text>Request permission</Text>
      </Pressable>

      <Pressable onPress={() => void handleToggleBubble()}>
        <Text>{bubbleState.isVisible ? 'Hide bubble' : 'Show bubble'}</Text>
      </Pressable>

      <Pressable onPress={() => incrementBubbleCount('app')}>
        <Text>+1 in app</Text>
      </Pressable>
    </View>
  );
}

Start here for the common path: request permission, show or hide the default bubble, and read synchronized state inside the app.

Custom renderer

tsx
import { useEffect } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import {
  type BubbleRendererProps,
  setBubbleRenderer,
} from 'expo-draw-over-apps';

function MyBubble({ state, increment, decrement, hide, openApp }: BubbleRendererProps) {
  return (
    <View style={styles.bubble}>
      <Text style={styles.label}>Counter</Text>
      <Text style={styles.count}>{state.count}</Text>

      <View style={styles.row}>
        <Pressable onPress={decrement} style={styles.button}>
          <Text style={styles.buttonText}>-</Text>
        </Pressable>
        <Pressable onPress={increment} style={styles.button}>
          <Text style={styles.buttonText}>+</Text>
        </Pressable>
      </View>

      <Pressable onPress={() => void openApp()} style={styles.secondaryButton}>
        <Text style={styles.secondaryText}>Open app</Text>
      </Pressable>

      <Pressable onPress={hide} style={styles.secondaryButton}>
        <Text style={styles.secondaryText}>Hide</Text>
      </Pressable>
    </View>
  );
}

export default function Screen() {
  useEffect(() => {
    setBubbleRenderer(MyBubble);
    return () => setBubbleRenderer(null);
  }, []);

  return null;
}

const styles = StyleSheet.create({
  bubble: {
    width: 200,
    borderRadius: 24,
    padding: 16,
    gap: 12,
    backgroundColor: '#111827',
  },
  label: {
    color: '#86efac',
    textAlign: 'center',
  },
  count: {
    color: '#ffffff',
    fontSize: 32,
    fontWeight: '800',
    textAlign: 'center',
  },
  row: {
    flexDirection: 'row',
    gap: 10,
  },
  button: {
    flex: 1,
    borderRadius: 16,
    paddingVertical: 12,
    alignItems: 'center',
    backgroundColor: '#2563eb',
  },
  buttonText: {
    color: '#ffffff',
    fontSize: 20,
    fontWeight: '800',
  },
  secondaryButton: {
    borderRadius: 14,
    paddingVertical: 10,
    alignItems: 'center',
    backgroundColor: '#e5e7eb',
  },
  secondaryText: {
    color: '#111827',
    fontWeight: '700',
  },
});

Use this when you want to keep the module's Android overlay plumbing but draw the bubble with your own React Native components.

Tailwind / NativeWind renderer

tsx
import { useEffect } from 'react';
import { Pressable, Text, View } from 'react-native';
import {
  type BubbleRendererProps,
  setBubbleRenderer,
} from 'expo-draw-over-apps';

function TailwindBubble({ state, increment, decrement, hide, openApp }: BubbleRendererProps) {
  return (
    <View className="w-52 rounded-[28px] bg-black px-4 py-4">
      <Text className="text-center text-[11px] font-semibold uppercase tracking-[2px] text-zinc-400">
        Tailwind bubble
      </Text>
      <Text className="mt-3 text-center text-4xl font-black text-white">{state.count}</Text>

      <View className="mt-4 flex-row gap-3">
        <Pressable onPress={decrement} className="flex-1 rounded-2xl border border-white/10 bg-zinc-900 py-3">
          <Text className="text-center text-xl font-bold text-white">-</Text>
        </Pressable>

        <Pressable onPress={increment} className="flex-1 rounded-2xl bg-white py-3">
          <Text className="text-center text-xl font-bold text-black">+</Text>
        </Pressable>
      </View>

      <Pressable onPress={() => void openApp()} className="mt-3 rounded-2xl bg-zinc-100 py-3">
        <Text className="text-center font-semibold text-black">Open app</Text>
      </Pressable>
      <Pressable onPress={hide} className="mt-2 rounded-2xl border border-white/10 py-3">
        <Text className="text-center font-semibold text-white">Hide bubble</Text>
      </Pressable>
    </View>
  );
}

export default function BubbleRendererRegistration() {
  useEffect(() => {
    setBubbleRenderer(TailwindBubble);
    return () => setBubbleRenderer(null);
  }, []);

  return null;
}

If your React Native app already uses NativeWind, you can register a bubble renderer built with Tailwind utility classes instead of a StyleSheet.

API

Every exported helper maps to a real overlay job in the Expo module.

From checking draw-over-apps permission to showing the bubble, syncing shared state, and swapping the renderer, the API is organized around the actual integration steps you use in an Expo Android app.

Permissions

Permission checks and the Android overlay settings handoff.

2 entries
canDrawOverlays(): boolean
requestPermission(): Promise<boolean>

Bubble visibility

Methods that control whether the floating surface is on screen and how the app is reopened.

4 entries
showBubble(): Promise<boolean>
hideBubble(): boolean
isBubbleVisible(): boolean
openApp(): Promise<boolean>

Shared state

Helpers for the synchronized counter and visibility state shared by the app and the bubble.

6 entries
incrementBubbleCount(source?: 'app' | 'bubble')
decrementBubbleCount(source?: 'app' | 'bubble')
setBubbleCount(count: number, source?: 'app' | 'bubble')
refreshBubbleState(): BubbleState
subscribeToBubbleState(listener): () => void
useBubbleState(): BubbleState

Customization

Types and entry points for replacing the default bubble UI with your own renderer.

3 entries
setBubbleRenderer(renderer: BubbleRenderer | null): void
BubbleRendererProps
BubbleState

Open source

Source, example app, and issue tracking stay connected to the module.

Every important destination stays obvious: the main repo for source and releases, the example app for implementation details, and issues for community feedback.