# Voltra: Live Activities and widgets from React Native JSX

> Voltra turns React Native JSX into SwiftUI and Jetpack Compose Glance, so a team can ship iOS Live Activities, Dynamic Island layouts and Android home screen widgets without writing native code, and a config plugin generates the native extension targets. It comes from Callstack, ships as two per-platform client packages, and the repository carries agent-oriented files alongside the code.

**callstackincubator/voltra** — Voltra lets React Native developers build native iOS Live Activities, Dynamic Island layouts, and widgets as React components without writing Swift. It supports hot reload, push updates, and includes a config plugin that wires everything automatically.

- Repository: https://github.com/callstackincubator/voltra
- Website: https://use-voltra.dev
- Stars: 851 · Forks: 50
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/callstackincubator-voltra

## The problem: a Live Activity is a second app surface

A Live Activity is the panel that appears on an iPhone's lock screen and in the Dynamic Island while something is happening, and an Android home screen widget is the same idea on the other side of the fence. Both are rendered by the system, outside your app's view hierarchy, and both are traditionally written in the platform's own UI language, SwiftUI and Jetpack Compose Glance.

Voltra's proposition is that you should not have to. The README puts it plainly: Voltra turns React Native JSX into SwiftUI and Jetpack Compose Glance so you can ship custom Live Activities, Dynamic Island variants, and Android Widgets without touching native code, with the config plugin handling the native extension targets.

The project comes from Callstack, the React and React Native consultancy, and the README says it is an open source collaboration between Saúl Sharma and Szymon Chmal. The homepage is use-voltra.dev, the licence is MIT, the primary language is TypeScript, the default branch is main, and the last push was on 2026-10-16. The recent releases are v2.3.1 on 2026-09-16, v2.3.2 on 2026-09-22 and v2.4.0 on 2026-10-06, so the 2.x line is moving.

It is worth being clear about the scope. This is not a way to write an entire iOS app in React, and it is not a general native view system. It covers two specific surfaces, Live Activities and Dynamic Island on iOS, and home screen widgets on Android, and the rest of your app stays a normal React Native application.

## The component model is React Native props

The quick example in the README shows the shape of the whole thing, and the styling is the first thing to notice:

```tsx
import { useLiveActivity, Voltra } from '@use-voltra/ios-client'

export function OrderTracker({ orderId }: { orderId: string }) {
  const ui = (
    <Voltra.VStack style={{ padding: 16, borderRadius: 14, backgroundColor: '#111827' }}>
      <Voltra.Text style={{ color: 'white', fontSize: 18, fontWeight: '700' }}>Order #{orderId}</Voltra.Text>
      <Voltra.Text style={{ color: '#9CA3AF', marginTop: 6 }}>Driver en route · ETA 12 min</Voltra.Text>
    </Voltra.VStack>
  )
```

The style prop is a plain React Native style object, with the same names you would use anywhere else in the app, padding, borderRadius, backgroundColor, color, fontSize, fontWeight. The feature list calls this familiar styling, and the promise is that you use React Native style props and platform-native modifiers in one place.

The components are namespaced under Voltra, so Voltra.VStack is a vertical stack and Voltra.Text is a text run, and the tree you build is a description of the surface rather than a live component. That is the right shape for this problem, because the system is the one that renders the view, not your app.

The two text lines in the example are a good demonstration of the payoff. The order identifier and the status line are the same components you would render in the app, and the same styles, so the lock screen panel and the in-app screen do not drift apart.

## useLiveActivity hands you start, update and end

The second half of the example is the hook that connects the description to the system:

```tsx
  const { start, update, end } = useLiveActivity(
    { lockScreen: ui },
    {
      activityName: `order-${orderId}`,
      autoStart: true,
      deepLinkUrl: `/orders/${orderId}`,
    }
  )
```

The first argument is the view, in this case the lock screen variant, and the second is the configuration. autoStart means the activity begins as soon as the component mounts, deepLinkUrl is what a tap on the activity opens, and activityName identifies the activity in the system.

Three functions come back, and they map onto the lifecycle the platform expects: start, update and end. The update function is the interesting one, because a Live Activity that never changes is not much use, and this is where a delivery app pushes new state into the lock screen panel as the order progresses.

One detail in the example is easy to miss and quite characteristic. The component returns null at the end. The component exists to describe the activity and to drive its lifecycle, not to render anything itself, because the system renders the surface. That also means the component is not part of your app's view hierarchy, so it will not push a frame to the screen, and a developer who expects to see it while working on it needs to know to look at the lock screen instead.

The development experience is handled by the same feature list entry that mentions payload budgets: the hooks respect Fast Refresh, so editing the component updates the live preview rather than requiring a rebuild.

## The config plugin generates the native extension targets

The installation is where the platform work happens, and it is done through the Expo config plugin rather than by hand.

The library is not supported in Expo Go, and the README is explicit that to set it up correctly you need the Expo Dev Client. The client packages are installed per platform:

```sh
npm install @use-voltra/ios-client
npm install @use-voltra/android-client
```

Then the plugins are added to the expo section of app.json, with the iOS plugin taking the group identifier and the push notification switch, and the Android plugin taking a list of widgets:

```json
"plugins": [
  [
    "@use-voltra/ios-client",
    {
      "groupIdentifier": "group.your.bundle.identifier",
      "enablePushNotifications": true
    }
  ],
```

The Android widget entries carry an id, a display name, a description and the target cell width and height, which are how a widget declares how large it wants to be on the home screen.

Finally, the native extension targets are generated with:

```bash
npx expo prebuild --clean
```

The clean flag matters, because prebuild regenerates the native projects and you want the extension targets regenerated consistently rather than patched into an existing directory. The plugin does the work that would otherwise mean adding an app extension target, signing configuration and widget provider by hand in Xcode and Android Studio.

And the feature list says it works with your setup, being compatible with both the Expo Dev Client and bare React Native projects, so the plugin is not tied to the Expo tooling even though the README's instructions use it.

## Push updates, and the budgets on both sides of the bridge

Two features decide whether a Live Activity is a real feature or a demo.

The first is production-ready push notifications, with support for ActivityKit push tokens on iOS and FCM on Android, to stream lifecycle updates and build server-driven refreshes. That is the difference between an activity that updates when the app happens to be running and one that updates from a server while the app is closed, which is the normal case for a delivery tracker or a flight status.

The second is the budget, and the feature list is careful about it: the hooks respect Fast Refresh, and both the JavaScript and the native layers enforce platform-specific payload budgets. The system limits how much data can cross the bridge, and a project that ignores the limit will find out at runtime rather than at compile time.

That is why the README frames the library as production-ready in that specific sense rather than as a general claim, and it is also why the documentation has separate iOS and Android sections for setup and development, along with separate API reference pages for the configuration of each platform. The two platforms are not the same surface with different names, and the library does not pretend otherwise.

The last feature entry is about the developer rather than the runtime. The Voltra schema, the hooks and the examples ship with TypeScript definitions, tests and docs, and the stated reason is so that AI coding agents stay productive. That is a revealing choice of wording for a project from a consultancy, and it connects to the files in the repository root.

## A monorepo with agent files, per-language formatters and an example app

The top-level listing shows what a project like this takes.

There is a pnpm workspace with pnpm-lock.yaml and a pnpm-workspace.yaml, the package manager pinned at pnpm 11.5.0 in the root package.json, and turbo.json for the task runner, with the scripts built around turbo run with a filter that excludes the website from the build, clean, lint and test passes. The packages/ directory holds the published packages, including the two client packages and the Expo plugin, and there is a website/ directory for the documentation site.

The formatting setup is the most detailed part, and it says a lot about the shape of the project. There is one formatter per language: Prettier for the JavaScript, ktlint for the Kotlin on the Android side, and swiftformat for the Swift on the iOS side, with the check, fix and staged variants for each. Linting is oxlint for JavaScript and TypeScript. lefthook.yml runs the git hooks, and there is a.swiftformat configuration and a.oxlintrc.json.

Then there are the agent-oriented files. There is an AGENTS.md and a CLAUDE.md at the root, and a skills/ directory, which together with the README's remark about AI coding agents suggests the project expects a large part of its contribution to come from assistants. There is a patches/ directory, which is where a monorepo keeps the patches it applies to dependencies, and a.changeset/ directory for the release notes, and a scripts/ directory for the automation.

The example/ directory is a full application rather than a snippet. It has an app/ directory, screens/, components/, hooks/, widgets/, notifications/, a server/ directory, an e2e/ directory, __tests__/, and its own jest.config.js, metro.config.js and rn-harness.config.mjs, along with app.json, eas.json and index.js. A server and an e2e suite next to the app is the shape you would expect for a library whose main use case is a delivery tracker with server-driven updates, and the React Native harness configuration is there so the same code can be exercised without a device.

## Conclusion

Adopt Voltra if your React Native app already renders a Live Activity or a widget in native code and you would rather keep that surface in the same codebase and the same component model, because the plugin keeps the JSX and the native layout in one place and the config plugin handles the extension targets. Do not adopt it for an Expo Go project, because the library is not supported there and needs the Expo Dev Client, and be aware that the native layers are still SwiftUI and Glance underneath, so a platform-specific visual will still need a native modifier. Before you commit, read the payload budget rules for your platform, because both the JavaScript and the native side enforce them, and test on a real device, since a Live Activity and a home screen widget only exist on hardware.

## FAQ

### What does Voltra let me build without native code?

iOS Live Activities, Dynamic Island layouts and Android home screen widgets, written as React Native components. Voltra turns that JSX into SwiftUI on iOS and Jetpack Compose Glance on Android, so you do not write Swift, Kotlin or Xcode and Android Studio user interface code for those surfaces.

### How do I install Voltra?

Install the client package for each platform you need, npm install @use-voltra/ios-client and npm install @use-voltra/android-client, add the two Expo plugins to your app.json with the iOS group identifier and the Android widget definitions, and then run npx expo prebuild --clean to generate the native extension targets. The library is not supported in Expo Go, so you need the Expo Dev Client.

### Can a Voltra Live Activity be updated from a server?

Yes. The project lists production-ready push notifications as a feature, with support for ActivityKit push tokens on iOS and FCM on Android, to stream lifecycle updates and build server-driven refreshes. The hook also returns start, update and end functions for driving the activity lifecycle from the app.

### Does Voltra work in a bare React Native project?

Yes. The README says it is compatible with the Expo Dev Client and with bare React Native projects, and that the platform config plugins wire the native extension targets for you, so the plugin is not tied to the Expo tooling even though the setup instructions use it.

## Sources

- [callstackincubator/voltra on GitHub](https://github.com/callstackincubator/voltra)
- [License: MIT](https://github.com/callstackincubator/voltra/blob/main/LICENSE)
- [Project website](https://use-voltra.dev)
- [README](https://github.com/callstackincubator/voltra/blob/main/README.md)
- [Releases](https://github.com/callstackincubator/voltra/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/callstackincubator-voltra
