Live Activities and Widgets with React: Say Hello to Voltra

Authors
Saúl Sharma
Software Engineer
@

If you’ve seen our recent demos on X or LinkedIn, you’ve already had a glimpse of what’s coming. Voltra is a new library that lets you build iOS Live Activities and Widgets using React. No Swift required.

It’s not a replacement for native code, and it doesn’t try to be. Instead, Voltra focuses on the 80% of real-world cases where a React-based solution is more than enough, without sacrificing correctness or performance.

Before diving into the code, it helps to understand what Live Activities and Widgets actually are. And why they exist in the first place.

Live Activities and widgets

Three different sizes of Widgets on iOS, iPadOS, macOS and CarPlay

Widgets have been with us for quite some time now. First introduced in iOS 14, they were designed to surface important information in a way that’s easily accessible to the user. Over the years, we’ve seen several UI and UX changes. Widgets moved from a dedicated screen to the Home Screen and, later, to the Lock Screen. Despite these changes, their core nature has remained the same: widgets are mostly static, refreshing periodically to display up-to-date data.

Live Activities, on the other hand, are a much more recent addition and fundamentally different in nature. They’re not meant to be persistent UI elements, but rather an on-the-spot experience, with a strong focus on being dynamic. They’re commonly used to show the current state of an ongoing process, such as a food delivery or a taxi ride.

Live Activities take two forms when displayed in the Dynamic Island on iPhone 14+

This dynamic nature comes with additional requirements. Live Activities need to be updated frequently, often even when the app isn’t running in the background. To make this possible, developers rely on push notifications to update their state. While both widgets and Live Activities are built on top of WidgetKit, push-based updates are limited to Live Activities  and that’s only one of several differences between the two. To keep this article at a reasonable length, I’ll stop here and encourage you to explore Apple’s documentation if you want to dive deeper.

Challenges with Live Activities in React Native

Up until now, developing Live Activities or widgets has been far from straightforward. While it was possible to add them to React Native apps, doing so required deep knowledge not only of React Native itself, but also of native iOS development. You needed to be comfortable with Swift and SwiftUI to build the UI, create your own TurboModule to expose native functionality to Hermes, and then wire everything together. Often hoping nothing would break along the way.

There have been attempts to make this process easier. One example is expo-live-activity, created by Software Mansion. While customization options were limited, the library enabled many apps to adopt Live Activities. For some teams, however, this wasn’t enough. More advanced use cases still required dropping down to native code.

Tools like expo-apple-targets, created by Evan Bacon, helped reduce some of the pain by allowing native code to live outside the native project directory, preventing it from being wiped during prebuilds. Still, the requirement to understand Swift remained, and for many developers around the world, that barrier alone was a deal-breaker.

Breaking the Swift barrier

Voltra approaches the problem from a different angle. Instead of offering a ready-made UI with a handful of toggles for customization, it lets you define the interface entirely on your own, using the same JSX you already use to build your React Native application.

🚀I built Voltra, an npm package that lets you:

ship custom Live Activities, Widgets, Dynamic Island + more with React.

No Swift, Xcode, or extra JS runtime needed.
⚡ Hot reload
⛽ Server push
✻ AI coding agent-friendly
𝝠 @Expo + React Native ready

Demo + must-read… pic.twitter.com/0L3IjFWEQJ

— Saúl (@saul_sharma) September 28, 2025

The idea for the library and its initial implementation comes from Saúl Sharma, who posted a nice demo of what’s possible with this approach a couple of months ago. However, he couldn’t fully focus on finishing Voltra, which is when I stepped in to complete what he had started.

At its core, Voltra’s approach is conceptually similar to React Native itself. You describe a native UI in JavaScript, pass that description to the native layer, and let the system construct the actual view hierarchy. In this case, using SwiftUI. This means you’re no longer constrained by a fixed set of predefined layouts or variants. With the primitives provided by Voltra, you can build your Live Activity UI from scratch using just React.

This approach introduces a significant advantage, in some cases even over a fully native implementation. Because the UI is defined declaratively and transmitted as data, you can update what’s displayed remotely via push notifications, without requiring users to download a new version of your app. Imagine a special in-app event scheduled hours in advance: instead of rushing an app update and hoping users install it in time, you can simply push a new Live Activity definition right before the event starts.

How Voltra transforms React components to intermediate representation and final Live Activity payload.

At a deeper level, Voltra adopts an approach similar to React Native’s legacy architecture, with the explicit goal of producing a SwiftUI view hierarchy. The JSX you write is converted into a JSON structure by a custom React renderer, compressed, and then passed to the native layer. Voltra’s native renderer then decompresses that payload and recreates the view tree, element by element, using SwiftUI primitives.

Voltra doesn’t aim to provide cross-platform abstractions, at least not yet. Instead, you work directly with SwiftUI concepts like HStack and VStack, while SwiftUI’s own layout engine takes care of positioning elements inside a Live Activity or widget. While this might sound straightforward, the reality is more complex. Voltra has to navigate a number of constraints along the way, from the laughably small 4 KB payload limit, through limited interaction handling, to subtle styling differences between environments.

We’ll dive into those challenges later. For now, let’s take a look at how you can start using Voltra.

Creating your first Live Activity with Voltra

Preparing your project

Voltra is an Expo Module and makes heavy use of the Expo Config Plugin system. If you’re using Expo, you’re good to go.

💡 If not, you can still use Voltra, but you’ll need to make some manual changes to your native project. We’re working on a guide for non-Expo setups, so stay tuned for updates.

For the purpose of this example, we’ll assume your app is an Expo app. Installing Voltra is straightforward and works with any package manager:

npm install voltra

It comes with an Expo Config plugin that sets up your native project correctly. To enable it, you’ll need to modify your app.config.js (or a similar configuration file):

{
  "expo": {
    "plugins": [
      "voltra"
    ]
  }
}

After that, run Expo prebuild (or your preferred method) to regenerate your native project. From that point onward, you can use Voltra’s API in your app.

Starting a Live Activity

Voltra offers two kinds of APIs: imperative and hook-based. In this example, we’ll use the hook-based approach, which supports hot reloads out of the box. In one of your components, add the following code:

import { Voltra, useLiveActivity } from 'voltra'

const activityUI = (
  <Voltra.VStack style={{ padding: 16, borderRadius: 18, backgroundColor: '#101828' }}>
    <Voltra.Symbol name="car.fill" type="hierarchical" scale="large" tintColor="#38BDF8" />
    <Voltra.Text style={{ color: '#F8FAFC', fontSize: 18, fontWeight: '600' }}>Driver en route</Voltra.Text>
    <Voltra.Text style={{ color: '#94A3B8', fontSize: 12, marginTop: 8 }}>Building A · Lobby pickup</Voltra.Text>
    <Voltra.Button title="Contact driver" id="contact-driver" style={{ marginTop: 12 }} />
  </Voltra.VStack>
)

// Start the live activity
useLiveActivity({
  lockScreen: activityUI,
})

This will create a new Live Activity when your component mounts and display it on the Lock Screen. The Dynamic Island, however, will remain empty, since we haven’t defined any UI for it yet. If you like, you can add an island property and provide a minimal UI, for example, a single symbol in its compact state, to be shown there. Consult Voltra’s documentation for the expected object structure.

Notice the components we’re using. They all come from voltra library and are direct equivalents of native SwiftUI views. Essentially, you’re defining a native view hierarchy using JSX within your React Native app.

Styling is handled via the style prop, just like in React Native. However, only a subset of style properties is supported. If you try to use unsupported properties, the TypeScript compiler will warn you, or you can check the documentation for the full list.

Once your app runs and the component mounts, you’ll see your Live Activity presented in all variants. You may notice minor layout differences - this is expected. Live Activities and widgets operate in severely constrained environments (limited memory and CPU time). Voltra relies on SwiftUI’s native layout engine rather than implementing custom layout logic like React Native’s Flexbox. It’s a small inconvenience, but one to be aware of as a Voltra user.

What about remote updates?

One of Voltra’s key strengths, and a feature that can even surpass the native solution, is the ability to display arbitrary Live Activities remotely, without requiring an app update. Because Voltra describes the view hierarchy as JSON, the same logic used in React Native can be executed on a server to create a blueprint of the activity, which is then delivered to the device.

{
  "aps": {
    "timestamp": 1765896094,
    "event": "start",
    "content-state": {
      "uiJsonData": "<<Voltra JSON>>"
    },
    "attributes-type": "VoltraAttributes",
    "attributes": { "name": "name-of-your-activity" },
  }
}

APNs payload to remotely start a Live Activity backed by Voltra.

Live Activities support remote updates via push notifications, so we can use this mechanism to send the JSON blueprint from a server to a device. On the device, Voltra uses the same native engine to render the JSON - from its perspective, there’s no difference between a description coming from the device itself or from a remote service.

To generate these descriptions, you can use Voltra’s server-side API. It renders the JSX, compresses it, and returns a base64-encoded string that can be embedded in a push notification and sent to the device. For details on how to structure the push notification itself, refer to Apple’s documentation.

Challenges Voltra solves

There's a handful of optimizations that the library is doing for you due to Live Activities limitations imposed by iOS, mainly regarding the 4 KB payload limit and preloading images. The description of the entire UI, including all variants, needs to fit in roughly 3,200 characters (assuming only ASCII characters), as the rest of the space is reserved by ActivityKit. That’s not much. In fact, it’s an incredibly tight constraint.

Converting Views and props to stable numeric IDs

Firstly, Voltra doesn’t send views as strings. Instead, it converts them to stable numeric IDs, immediately saving around 20%–30% of space. A similar approach is applied to props and style properties. The result is a JSON string composed mostly of numbers and short strings. While we can define all variants, they’re initially constrained to display only a handful of views. That’s where text compression comes in.

Compression payload with Brotli

By using Brotli lossless compression, Voltra can shrink the payload by roughly 60%, even after serializing the compressed byte stream into a base64 string, which is inherently less space-efficient. At this point, more complex UI structures are possible, but there’s still room for optimization.

Reusing JSX elements registry

In many cases, the same UI elements are repeated multiple times. For example, consider a GitHub commits graph composed of squares in different shades of green. Each square is identical in structure, but without optimization, Voltra would describe each instance individually, wasting space.

Thankfully I found a better approach: when you define a JSX element once and reuse the same reference, Voltra detects it and stores it in an element registry. Every subsequent instance references the original definition. This optimization allowed the GitHub example to grow from 4 rows to over 12. You can apply the same pattern in your layouts to create as complex UI as you need.

Preloading images

Another challenge Voltra tackles is displaying images. Live Activities don’t have access to the internet, so network images can’t be fetched on the fly. You might wonder how apps like Uber show driver photos. The answer is preloading: the image is loaded in the app, saved in a location accessible to the Live Activity, and then displayed from that persistent storage. Voltra provides an image preloading API to make this simple. You can preload an image in your React Native code and then render it in your Live Activity - all without writing native code yourself.

Is Voltra production-ready?

Voltra should be considered an experimental technology in a public preview stage. It is expected to work correctly, but it hasn’t yet been battle-tested in production. As a result, you may encounter bugs along the way. This is entirely normal for a preview release.

That said, I strongly encourage you to try Voltra. If you run into any issues, please feel welcome to open an issue on the Voltra repository. Together, we can refine the library, and ultimately create a robust solution for Live Activities and widgets in React Native.

What’s next for Voltra?

We're only getting started and have much more up our sleeves:

Expanded feature coverage‍

Support for more native views, properties, and styling options. Community feedback will guide what gets prioritized.

‍Simpler integration‍

A potential move to a TurboModule, removing the need for Expo Modules and improving integration in non-Expo projects.

‍Android support‍

Planned for the future once the core experience is fully polished. This will involve defining Android-specific primitives and implementing the native engine in Kotlin.

Wrapping up

This is Voltra, the all-in-one library for Live Activities and widgets in React Native. It doesn’t constrain you to a set of predefined layouts; instead, it gives you the freedom to design any UI, as long as it’s built from composable SwiftUI primitives.

The public preview is now live. You can install Voltra from NPM using your favorite package manager and start experimenting right away. Head over to Voltra’s documentation page to follow the setup instructions.

I’m really excited to hear about your experience and what you'll build with the library! There’s a showcase discussion in Voltra’s repository where you can show what you’ve built. I’m genuinely curious to see what your imagination comes up with.

Finally, contributions and feedback are welcome! File issues and pull request on Voltra's GitHub repository, contact us via Callstack’s Discord (#voltra channel) or DM me on X or LinkedIn.

Happy coding!

Insights

Learn more about React Native

cover

Aug 4Podcast

VisionCamera V5 With Marc Rousavy

Marc Rousavy joins Mike Grabowski to unpack VisionCamera v5, Nitro Modules, the constraints API, migration work, and what changed for React Native camera apps.

cover

Jul 1Podcast

Redraw for Effects Beyond Skia

William Candillon talks about where Redraw fits next to Skia, from physics-based rendering to camera effects, AR, and new UI patterns.

cover

Jun 29Article

Less Boilerplate, More Type Safety: React Navigation 8's Typed Hooks

React Navigation 8 improves typed hooks with screen-aware types, less boilerplate, and fewer unsafe casts in shared UI and nested navigators.

cover

Apr 30Live Stream

Build a React Native App With the TanStack Stack

Join Callstack’s live stream on building a React Native app from the ground up using the TanStack toolbelt.

cover

Apr 28Article

React Native production lessons from React Universe Meetup x Zalando

Lessons from React Universe Meetup x Zalando on React Native in production, covering brownfield performance, feed video, native API access, and on-device AI.

cover

Apr 23Talk

Shipping iOS Live Activities with React: The Voltra Story

How Voltra enables Live Activities and Dynamic Island views using React without writing Swift. A deep dive into iOS constraints and architecture.

cover

Apr 16Live Stream

React Native 0.85: Animation Backend and Key Updates

A recap of React Native 0.85, covering the new animation backend, DevTools improvements, and ecosystem takeaways.

cover

Mar 5Article

Keeping Screens Visible With <Activity mode="hidden">

A cross‑platform workaround to keep screens visible while <Activity mode="hidden"> unmounts effects on inactive screens by setting mode="hidden".

cover

Feb 25Article

React Native Monorepo With pnpm Workspaces

Set up a React Native monorepo with pnpm workspaces for mobile and Next.js web, including Metro fixes, hoisted node-linker, and shared packages.

cover

Feb 18Article

A Practical Guide to React Native Monorepo With Yarn Workspaces

Learn how to set up a React Native monorepo with Yarn workspaces for mobile and web, including Metro, Gradle, and shared packages.

cover

Feb 17Podcast

From Teddy Bears to Voice Agents: Kraen Hansen on Voice AI, Local-First & App Security

What happens when you combine voice agents and teddy bears? A hands-on look at local-first systems, voice AI, and security lessons from poking real apps.

cover

Jan 28Article

Bringing CSS Clipping to React Native

A deep look at how a single CSS feature moves through React Native’s New Architecture, from JavaScript styles to native rendering on iOS and Android.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.