Android only
Focused on overlays, permissions, and service-driven UI instead of pretending to be a broad cross-platform abstraction.
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
In-app controls
Overlay permission
Granted
Bubble
Visible
Shared count
14
Install
npm install expo-draw-over-appsUse a dev build, custom client, or standalone Android build. Expo Go is not supported.
Event stream
What ships
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
Start with canDrawOverlays(), send users into settings only when needed, then refresh when the app becomes active again.
Renderer swap
setBubbleRenderer() gives you a narrow seam for customization without throwing away the working Android overlay plumbing.
Realtime sync
useBubbleState(), refreshBubbleState(), and the counter helpers make the overlay feel like part of the app instead of a detached widget.
Expo fit
This module ships native Android code, so the docs stay honest about where it runs and where it does not.
Open source shape
That keeps the website useful without becoming a documentation island that drifts away from the repository.
How it works
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.
Use canDrawOverlays() to know whether Android already allows the overlay window.
Call requestPermission() only when permission is missing, then wait for the app to become active again.
showBubble() makes the floating UI visible and ready to move above other apps.
Counter updates and visibility changes stay in sync through the shared state helpers.
Quick start
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-appsUse 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
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.
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.
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
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.
Permission checks and the Android overlay settings handoff.
canDrawOverlays(): booleanrequestPermission(): Promise<boolean>Methods that control whether the floating surface is on screen and how the app is reopened.
showBubble(): Promise<boolean>hideBubble(): booleanisBubbleVisible(): booleanopenApp(): Promise<boolean>Helpers for the synchronized counter and visibility state shared by the app and the bubble.
incrementBubbleCount(source?: 'app' | 'bubble')decrementBubbleCount(source?: 'app' | 'bubble')setBubbleCount(count: number, source?: 'app' | 'bubble')refreshBubbleState(): BubbleStatesubscribeToBubbleState(listener): () => voiduseBubbleState(): BubbleStateTypes and entry points for replacing the default bubble UI with your own renderer.
setBubbleRenderer(renderer: BubbleRenderer | null): voidBubbleRendererPropsBubbleStateOpen source
Every important destination stays obvious: the main repo for source and releases, the example app for implementation details, and issues for community feedback.
Browse the module source, README, release history, and the current docs code.
Inspect the working Expo example that requests permission, shows the bubble, and syncs the counter.
Collect bugs, feature requests, integration edge cases, and setup feedback in one public place.