expo-quick-actions: home screen shortcuts and dynamic icons for Expo apps
Add home screen quick actions / shortcuts and custom icons to your Expo app
At a glance
- What is it?
- expo-quick-actions is a community React Native library that adds home screen quick actions and dynamic app icons to an Expo app, with a typed Action object, hooks for React, and an Expo Router integration that routes a shortcut to a URL. It suits an Expo team that wants the platform shortcut API without writing native code, and it does not suit an app that has already moved off Expo or a product that needs SF Symbols on Android.
- Who is it for?
- Adopt expo-quick-actions if your app is on Expo and you want press-and-hold shortcuts and a switchable app icon without writing Swift or Kotlin, and you are willing to pin the library version to your Expo SDK. Do not adopt it for a bare React Native project, where the platform APIs are reachable directly, or if you need one icon set across platforms, since SF Symbols are restricted to iOS.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 28 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two platform features, one JavaScript API
expo-quick-actions is a comprehensive React Native library for home screen quick actions and dynamic app icons, and it covers two separate platform capabilities that usually need native code.
The first is the quick action, the menu that appears when a user presses and holds the app icon. The second is the dynamic app icon, the ability to ship more than one icon and switch between them, for example to badge a state onto the icon without shipping a new build.
The first line of the README is the one to read before anything else: this is not an official Expo SDK package. It is a community library that follows the shape of an Expo module, with the native code in ios/ and android/, the config plugin in plugin/, and the JavaScript surface in src/ and build/. That distinction matters for support and for how quickly it will follow an SDK release.
The audience is an Expo application team that wants the platform behaviour without the Swift and Kotlin work. The library's own examples live in example/, including App.tsx and a dedicated AppIconTest.tsx for the icon side.
What it gives you is a typed JavaScript surface over two things that are otherwise native APIs, and the interesting parts are in the details of that surface: how icons are addressed, how a shortcut carries a route, and how the library stays in step with an Expo SDK that changes every quarter.
The version table decides whether it works with your SDK
The library ships a compatibility table, and the instruction above it is blunt: ensure you use versions that work together.
The pairing runs like this. Expo 57.0.0 goes with expo-quick-actions 6.1.0, Expo 56.0.0 with 6.0.2, Expo 54.0.0 with 6.0.0, Expo 53.0.0 with 5.0.0, Expo 52.0.0 with 3.0.0 or later, Expo 51.0.0 with 2.0.0, Expo 50.0.0 with 1.0.0 and Expo 49.0.0 with 0.0.0.
So the library version tracks the Expo SDK version, which is what you would expect from a module that contains native code compiled against the SDK's own headers. The practical consequence is that an Expo upgrade is not a solo decision. When the SDK moves, this library needs a matching release, and the table is the place to check whether one exists before you start the upgrade.
The package manifest shows the current state: the version is 6.1.0, the description is use native quick actions and custom app icons, and the author is Evan Bacon. The licence is MIT.
There is a detail in the packaging that is worth understanding, because it explains how the library behaves in different environments. The exports map for the main entry has three conditions: types, a react-native condition pointing at build/index.js, and a default pointing at build/index.web.js. The web build is a separate artefact rather than a stub that throws, which is the difference between a module that breaks your web bundle and one that degrades.
The Action type is the whole contract with the platform
The public API starts with the Action type, and it is a small object with a few precise fields.
The id is a unique identifier for the action. The title is its label. The subtitle is optional and iOS only. The icon is an asset reference, and the icon section explains the prefixes in detail. The params field holds additional serial parameters for the action, and its type is a record of numbers, strings, booleans or null.
That params field is the interesting one, because it is how you carry routing information across the native boundary. A quick action has no URL, only a bag of values, and the library leaves the interpretation of that bag to you, which is what makes the same action work for a router, for an analytics event, or for a deep link.
The rest of the API is four more pieces. `initial` is a static property that returns the initial quick action item that was used to open the app, if any, and it is how you handle a cold start that came from a shortcut. `maxCount` is a static property returning the maximum number of quick action items that can be set, hardcoded to 4 on iOS and dynamically collected on start-up on Android. `setItems` is the async function that sets the items, and both Apple and Android recommend a maximum of 4 items. `isSupported` returns whether the device supports quick actions at all. `addListener` registers a callback that fires when a quick action is triggered and returns a subscription.
A worked example shows how a shortcut carries a route:
QuickActions.setItems([
{
id: "0",
title: "Open Settings",
subtitle: "Go here to configure settings",
icon: "heart",
params: { href: "/settings" },
},
]);Icon names are resolved through magic prefixes
On iOS there are three kinds of image for a shortcut icon, and the library addresses all three with a prefix in the icon string.
The symbol: prefix goes to UIApplicationShortcutIcon(systemImageName:), so symbol:heart.fill renders the SF Symbols icon named heart.fill. The documentation is explicit about the limit here: SF Symbols are only available on iOS, as Apple copyright restricts using them on other platforms.
A bare name is looked up in the list of system defined icons, and the list is given: compose, play, pause, add, location, search, share, prohibit, contact, home, markLocation, favorite, love, cloud, invitation, confirmation, mail, message, date, time, capturePhoto, captureVideo, task, taskCompleted, alarm, bookmark, shuffle, audio and update. If no built-in icon matches, the name is treated as a custom image, so heart will use UIApplicationShortcutIcon(templateImageName: "heart").
The asset: prefix is the explicit form of that fallback, going to UIApplicationShortcutIcon(templateImageName:) and loading the image of that name from the app bundle, masked to the system defined icon style.
The result is a single string that can carry any of the three kinds without the caller needing to know which one it is. For Android, the documentation points at the Android design docs for shortcut icons rather than reproducing a list, which is a sensible division since the two platforms have different icon vocabularies.
One inconsistency is worth flagging for anyone reading the API. The text says the iOS maximum is hardcoded to 4, while the code example for maxCount comments that it is e.g. 15 on Pixel 6, null on iOS. The declared type is number | null, so both readings are representable, but which one you get on iOS is something to check on the platform you ship to rather than assume.
Hooks for global effects, hooks for local state
The convenience hooks live behind a nested import, expo-quick-actions/hooks, and the split between the two is a design decision worth understanding.
useQuickActionCallback registers a callback that fires when a quick action is triggered, and the documentation describes it as useful for global updates like navigating or analytics. The example comment makes the point explicitly: do something with the action without explicitly re-rendering the component.
useQuickAction returns the current quick action, and is described as useful for updating the UI, with the note that it re-renders the component when the action changes.
So one hook is for side effects that should not disturb the render tree, and the other is for state that the tree should react to. Using the state hook for navigation would re-render the whole tree on every shortcut, and using the callback hook for a badge counter would leave the badge stale.
The Expo Router section shows the intended pattern for a routed application. The philosophy in Expo Router is to treat opening the app from the home screen as linking to the / index route, and the author says he would like to account for launching from native APIs like Share Sheet invocations, Siri, Quick Actions and Notifications, linking to a well known URL convention, and that this is not the case today but that the design accounts for it.
The mechanism is to pass params: { href: "..." } and use a hook in the layout route. The example imports useQuickActionRouting and RouterAction from expo-quick-actions/router and calls useQuickActionRouting() in the layout, then defines quick actions with an href param, including one that links outside the app:
import { useEffect } from "react";
import { Slot } from "expo-router";
import { useQuickActionRouting, RouterAction } from "expo-quick-actions/router";
import * as QuickActions from "expo-quick-actions";Dynamic app icons ship as a config plugin
The icon half of the library is a separate entry point, expo-quick-actions/icon, and in the package exports it also exposes a plugin path, ./icon/plugin, which points at plugin/build/withAppIcon.js.
That is the shape of an Expo config plugin. A config plugin runs at prebuild time and writes native configuration into the generated iOS and Android projects, which is how the alternative icons get registered with the operating system at build time. It is the same mechanism the dynamic icon feature of the app plugin uses, and the root app.plugin.js and the app.plugin export are there for the same purpose.
The package exports map shows the whole surface: the root entry with its react-native and web conditions, ./hooks, ./router, ./icon, the raw expo-module.config.json, the icon plugin, and the app plugin.
The build tooling follows the Expo module conventions. The scripts are expo-module build, clean, lint, test, prepare and prepublishOnly, with expo-module as the base command, and the Jest configuration uses the expo-module-scripts preset with passWithNoTests enabled. The published files list includes build, plugin/build, app.plugin.js, expo-module.config.json, ios and android, with the android build directory excluded.
The reason the icon work needs a plugin rather than a JavaScript call is worth understanding. Changing the installed icon is a native operation on both platforms, and the app has to declare the alternative icons to the system when it is installed. Once that is done, the switching at runtime is the part the JavaScript API handles.
Where the library stops, and the alternative
Three limits are visible in the documentation itself.
The first is that this is a community package rather than an official Expo SDK package, and it is versioned against SDK releases, so an Expo upgrade and a library upgrade are the same decision. The second is that the icon vocabulary is not portable: SF Symbols are restricted to iOS by Apple copyright, and the built-in system icon list is the iOS list, so an Android user sees a different set of shapes for the same action names. The third is the item limit, four on iOS and a platform defined number on Android, with both vendors recommending a maximum of four.
The alternative for a project that has moved off Expo is the platform APIs directly, and the difference in approach is exactly what the library exists to remove. On iOS that means working with UIApplicationShortcutItem and UIApplicationShortcutIcon in Swift, and on Android with the shortcut API in Kotlin, including the manifest entries that declare the shortcuts to the system. A bare React Native application can do that, and a native team may prefer to, because the shortcut behaviour then lives next to the rest of the native code and is not tied to an Expo SDK version.
For an Expo application, the opposite holds. The native code has to be written and the build has to be run through prebuild, and the config plugin approach means somebody has already solved that problem and shipped the solution for a set of SDK versions.
There is a middle option for an Expo app that is about to graduate: use the library while you are on Expo, and treat the shortcut definitions as data rather than as code, so that moving to native later means re-implementing a list of titles, icons and parameters rather than re-architecting a feature.
Editorial conclusion
Adopt expo-quick-actions if your app is on Expo and you want press-and-hold shortcuts and a switchable app icon without writing Swift or Kotlin, and you are willing to pin the library version to your Expo SDK. Do not adopt it for a bare React Native project, where the platform APIs are reachable directly, or if you need one icon set across platforms, since SF Symbols are restricted to iOS. Verify first that the version table pairs your Expo SDK with a matching expo-quick-actions release, that the dynamic icon plugin is applied in your app config, and that the shortcut count you plan to use fits inside the four item limit both platforms recommend.
Frequently asked questions
Which versions of expo-quick-actions work with my Expo SDK?
The compatibility table pairs Expo 57.0.0 with expo-quick-actions 6.1.0, Expo 56.0.0 with 6.0.2, Expo 54.0.0 with 6.0.0, Expo 53.0.0 with 5.0.0, Expo 52.0.0 with 3.0.0 or later, Expo 51.0.0 with 2.0.0, Expo 50.0.0 with 1.0.0 and Expo 49.0.0 with 0.0.0.
Is expo-quick-actions an official Expo package?
No. The README states at the top that this is not an official Expo SDK package. It is a community library that follows the Expo module layout, with native code in ios/ and android/ and a config plugin in plugin/.
How do I set home screen quick actions in expo-quick-actions?
Call QuickActions.setItems with an array of Action objects, each with an id, a title, an optional subtitle, an optional icon and optional params. Both Apple and Android recommend a maximum of 4 items, and QuickActions.maxCount reports the limit, hardcoded to 4 on iOS and collected at start-up on Android.
How do I use SF Symbols or a custom image as a quick action icon?
Prefix the icon with symbol: for an SF Symbol such as symbol:heart.fill, or with asset: for a custom bundled image such as asset:heart. A bare name such as compose is looked up in the list of built-in iOS system icons, and falls back to a custom image of the same name if none matches.
How do I handle a quick action that opens the app?
QuickActions.initial returns the initial quick action item that was used to open the app, if any, for a cold start, and addListener gives you a subscription that fires when a quick action is triggered. In React, useQuickActionCallback registers a callback for global effects such as navigation or analytics, and useQuickAction returns the current action so a component can re-render.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/evanbacon-expo-quick-actions)Was this useful?