npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-draw-over-apps

v55.0.2

Published

Expo module for Android overlay permissions and floating React Native or Jetpack Compose bubbles.

Readme

expo-draw-over-apps

expo-draw-over-apps is an Expo module for Android that lets your app:

  • check and request the draw over other apps permission
  • show a draggable floating bubble over other apps
  • keep a shared counter in sync between the app and the bubble
  • open the app from the bubble
  • render the bubble with React Native components
  • switch the bubble to a native Jetpack Compose renderer powered by @expo/ui
  • replace the default bubble UI with your own styled React Native component

This module is currently Android only.

Features

  • Android overlay permission helpers
  • Floating bubble overlay rendered with React Native or Jetpack Compose
  • Draggable bubble window
  • Shared realtime bubble state
  • Long-press menu to remove the bubble
  • Custom bubble renderer support with StyleSheet, NativeWind, or expo-ui

Open Source

Installation

bun add expo-draw-over-apps

If your app does not already use Expo modules in a bare React Native app, install Expo modules first:

Install Expo modules in a React Native project

Expo app requirements

This package contains native Android code, so it does not work in Expo Go.

Use one of these:

  • a development build
  • a custom client
  • a standalone Android build

If you are using Expo managed workflow, run prebuild/build as usual for native modules.

Android setup

The module ships with the required Android manifest entries:

  • android.permission.SYSTEM_ALERT_WINDOW
  • the internal overlay service

No extra manual manifest setup should be required after autolinking.

Quick start

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={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>
  );
}

Permission flow

requestPermission() opens the Android overlay permission screen when permission is missing.

Typical flow:

  1. Call canDrawOverlays().
  2. If it returns false, call requestPermission().
  3. When the app becomes active again, call canDrawOverlays() one more time.
  4. Only call showBubble() after permission is granted.

Bubble behavior

The built-in bubble:

  • can be dragged around the screen
  • exposes + and - counter actions
  • can open the app
  • can be hidden
  • shows a long-press remove menu

The default bubble uses React Native UI.

If you want the same counter overlay rendered with native Android widgets, use the packaged Compose renderer:

import { useEffect } from 'react';
import { setBubbleRenderer, setComposeBubbleRenderer } from 'expo-draw-over-apps';

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

  return null;
}

That renderer uses @expo/ui/jetpack-compose inside the overlay surface and prefers Android system icons for its action buttons when they are available.

Custom bubble UI

You can replace the default bubble renderer with your own React Native component by calling setBubbleRenderer.

That means you can style it with:

  • React Native StyleSheet
  • inline styles
  • NativeWind className if your host app already has NativeWind configured

If you want a ready-made native Android renderer instead of building your own React Native one, call setComposeBubbleRenderer().

Example

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',
  },
});

NativeWind example

If your app already uses NativeWind, your custom renderer can use className too:

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

export function TailwindBubble({ state, increment, decrement }: BubbleRendererProps) {
  return (
    <View className="w-48 rounded-3xl bg-slate-900 p-4">
      <Text className="text-center text-xs font-bold uppercase tracking-wider text-emerald-300">
        Counter
      </Text>
      <Text className="text-center text-4xl font-black text-white">
        {state.count}
      </Text>
      <View className="mt-3 flex-row gap-2">
        <Pressable className="flex-1 items-center rounded-2xl bg-orange-500 py-3" onPress={decrement}>
          <Text className="text-xl font-black text-white">-</Text>
        </Pressable>
        <Pressable className="flex-1 items-center rounded-2xl bg-blue-600 py-3" onPress={increment}>
          <Text className="text-xl font-black text-white">+</Text>
        </Pressable>
      </View>
    </View>
  );
}

API

canDrawOverlays(): boolean

Returns whether the app currently has overlay permission.

requestPermission(): Promise<boolean>

Opens the Android overlay permission screen when needed.

Returns:

  • true if permission is already granted
  • false if Android opened settings and the user still needs to grant it

showBubble(): Promise<boolean>

Shows the floating bubble.

Returns true when the request was accepted.

hideBubble(): boolean

Hides the floating bubble.

isBubbleVisible(): boolean

Returns the current known visibility state.

openApp(): Promise<boolean>

Brings the app back to the foreground from the bubble.

incrementBubbleCount(source?: 'app' | 'bubble'): number

Adds 1 to the shared counter.

decrementBubbleCount(source?: 'app' | 'bubble'): number

Subtracts 1 from the shared counter, without going below 0.

setBubbleCount(count: number, source?: 'app' | 'bubble'): number

Sets the shared counter to a specific value.

refreshBubbleState(): BubbleState

Refreshes the shared bubble state from native state.

subscribeToBubbleState(listener): () => void

Subscribes to bubble state changes.

useBubbleState(): BubbleState

React hook for the shared bubble state.

setBubbleRenderer(renderer: BubbleRenderer | null): void

Registers a custom React Native bubble renderer.

Pass null to restore the default renderer.

setComposeBubbleRenderer(): BubbleRenderer

Registers the packaged Jetpack Compose counter overlay renderer built with @expo/ui.

Types

BubbleState

type BubbleState = {
  count: number;
  isVisible: boolean;
  lastUpdatedAt: number;
  lastChangeSource: 'app' | 'bubble';
};

BubbleRendererProps

type BubbleRendererProps = {
  state: BubbleState;
  increment(): number;
  decrement(): number;
  setCount(count: number): number;
  hide(): boolean;
  openApp(): Promise<boolean>;
};

Notes

  • Android only
  • not supported in Expo Go
  • overlay permission must be granted by the user
  • the floating bubble is hosted by an Android service
  • custom bubble UI can use React Native components, or you can opt into the packaged Jetpack Compose renderer

Example app

The repo includes a working example app in example/ showing:

  • permission handling
  • show/hide bubble
  • shared realtime counter updates
  • custom bubble styling
  • live switching between React Native views and native Jetpack Compose
  • open app from bubble
  • long-press remove menu

Documentation website

The repo now includes a standalone Next.js documentation site in docs/.

Run it locally:

npm run docs:install
npm run docs:dev

Build the static export:

npm run docs:build

That will generate the static site in docs/out.

If you prefer running the docs app directly:

cd docs
npm install
npm run dev

Development

npm run build

To build the Android example app:

cd example/android
./gradlew assembleDebug

On Windows:

cd example\android
.\gradlew.bat assembleDebug