Skip to content

React Native

Building SwiftUI apps with Expo UI

Want iOS 26 Liquid Glass in a React Native app without writing Swift? How Expo UI puts real SwiftUI views inside Expo, with a working example.

Hau Le8 min read

Want Liquid Glass in your app but don't know how to write SwiftUI? This post is for you.

The official resources, if you'd rather go straight to the source:

Before we start

Hi everyone. This is a quick write-up of something I only started exploring recently, so if anything here looks off, let's talk it through.

Straight to the point, then. Look at the design below. Do you see a problem?

Design of an 'Add debt' screen in a debt-tracking app: an orange header with round translucent back and Next buttons, an amount card showing 100,000 VND, a borrow date field, and a frosted-glass calendar popover open over the content
The design: glass buttons and a frosted calendar popover, straight out of iOS 26.

With iOS 26, Apple shipped a new UI with modern materials and interactions: Liquid Glass. It isn't just a "prettier blur". It's a shift in design language and UI primitives, where animation, material, depth and interaction are all tied to the native rendering engine.

More importantly, Apple is moving the UI and UX of its whole ecosystem to this new design language. That tells us it's a long-term strategy, not a one-off visual refresh.

So what's the problem?

The platform's UI layer is changing fast, and we developers are left running to keep up.

If you build apps with React Native or Expo, you run into a few challenges:

If the platform is investing in SwiftUI primitives, how does our React Native app get to those primitives?

  • The new effects only exist in SwiftUI (iOS) or Jetpack Compose (Android).
  • There aren't many JS libraries that reproduce the real native behaviour, and the ones that exist each cover a single feature.
  • Most current solutions just imitate the look with JS and CSS-like styling.
  • Rewriting everything in Swift? Not realistic for teams that already have a large React Native codebase.

This is where @expo/ui comes in: it lets us use everything Expo can do, without building custom native modules or pulling in third-party libraries.

What is Expo UI, and why use it?

I won't cover what React Native or Expo are here; I'm sure you know them well by now.

And of course @expo/ui wasn't made only for Liquid Glass. It gives developers access to the platform's native primitives as soon as a new OS version ships. Liquid Glass is just the easiest case to see.

Instead of re-implementing UI in JavaScript, Expo UI exposes SwiftUI (iOS) and Jetpack Compose (Android) directly inside React Native. That means you can use the latest native UI primitives, including materials and interactions, without leaving the Expo ecosystem.

Expo UI was introduced in Expo SDK 53, but it's with SDK 54 that @expo/ui supports almost the full set of SwiftUI (some views and modifiers are still on the roadmap), rather than a few small components like Switch, Button and Slider as in the previous version.

How it works

SwiftUI primitives, not a new UI library

Expo UI isn't a new UI library. It brings SwiftUI's primitives into Expo and React Native, so you get native iOS UI and better performance.

That makes it different from earlier libraries like react-native-elements: Expo UI maps React components 1-to-1 to native SwiftUI views instead of simulating them in JS.

1-to-1 mapping

Expo UI components correspond almost directly to SwiftUI views. If you know SwiftUI, you'll pick Expo UI up quickly, and you can read SwiftUI documentation and apply it the same way. Two references I use:

Full-app support

Expo UI is designed to be used throughout an app, not just for a few isolated components. You can build a whole app with it and still stay flexible:

Here a native SwiftUI date picker sits inside an ordinary React Native card:

<View className="bg-background mb-6 flex flex-row rounded-xl px-6 py-4">
  <View className="flex-1 gap-3">
    <Text className="text-sectionTitle text-sm font-lexendSemiBold">
      {t("createDebtBorrowDateLabel")}
    </Text>
    {/* SwiftUI component */}
    <Host matchContents>
      <DateTimePicker
        variant="automatic"
        color="#FF7A45"
        onDateSelected={(date) => console.log(date)}
      />
    </Host>
  </View>
</View>

Expo UI by example

Install

npx expo install @expo/ui

Layout structure

<Host>: Expo UI uses <Host> as the root container for SwiftUI. Think of it as the bridge entry point that lets SwiftUI run inside Expo, so every SwiftUI component has to be wrapped in one.

Inside it, <VStack> and <HStack> lay views out in a column or a row, as they do in a SwiftUI hierarchy.

Putting React Native components inside SwiftUI

SwiftUI components only work under a <Host>. React Native components belong to a different UI tree, so putting one inside a <Host> usually doesn't work properly or fails to render.

Rendering a React Native component means you've left the SwiftUI context. If you want to use Expo UI components again after that, wrap them in a new <Host> to bring the SwiftUI context back.

Doesn't render:

<Host>
  <VStack>
    <Slider />
  </VStack>

  {/* React Native component */}
  <Text>React Native footer</Text>
</Host>

Works:

<View>
  <Host>
    <VStack>
      <Slider />
    </VStack>
  </Host>

  <Text>React Native footer</Text>
</View>

An example I built: an adaptive glass button

This button uses the native SwiftUI glass button where Liquid Glass is available, and falls back to a blurred React Native button everywhere else (older iOS, Android, Expo Go):

import { Host, Button, Image, Label, Text } from "@expo/ui/swift-ui";
import { BlurView } from "expo-blur";
import Constants from "expo-constants";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { useColorScheme } from "nativewind";
import React from "react";
import { Pressable, View } from "react-native";
import { type SFSymbol } from "sf-symbols-typescript";

// Icons instruction
// https://www.figma.com/design/ffyL6r98YI4HFkXmu9TRRO/SF-Symbols-%7C-Text-objects-for-Figma--Community-?node-id=2004-3&t=ui1HTrkThWzLB9ab-0

export function AdaptiveGlassButton({
  children,
  onPress,
  disabled,
  color,
  icon,
  title,
}: {
  children?: React.ReactNode;
  onPress?: () => void;
  disabled?: boolean;
  color?: string;
  icon?: SFSymbol;
  title?: string;
}) {
  const useGlass = isLiquidGlassAvailable();
  const isExpoGo = Constants.appOwnership === "expo";

  const theme = useColorScheme().colorScheme;

  return useGlass && !isExpoGo ? (
    <Pressable onPress={onPress} disabled={disabled} style={{ opacity: disabled ? 0.5 : 1 }}>
      <Host matchContents>
        {title ? (
          <Button variant="glass" controlSize="regular">
            <Label title={title} />
          </Button>
        ) : (
          <Button variant="glass" controlSize="regular">
            <Image
              systemName={icon ? icon : "arrow.backward"}
              color={color}
              frame={{ width: 20, height: 30, alignment: "center" }}
            />
          </Button>
        )}
      </Host>
    </Pressable>
  ) : (
    <Pressable
      onPress={onPress}
      disabled={disabled}
      className="w-11 h-11 overflow-hidden rounded-full"
      style={{ opacity: disabled ? 0.5 : 1 }}
    >
      <BlurView
        intensity={10}
        tint={theme === "light" ? "systemThinMaterialDark" : "extraLight"}
        className="absolute inset-0"
      >
        <View className="flex-1 items-center justify-center">{children}</View>
      </BlurView>
    </Pressable>
  );
}

And here's the result

Expo UI doesn't stop at the look. It also uses native behaviour: touch, press and drag feedback come from the platform itself. On the latest iOS, with Liquid Glass, buttons and interactions get the real native effect instead of a JS imitation.

These effects aren't always available, though. Android, or older iOS versions, may not fully support them. So when you build a cross-platform app, you usually need a fallback mechanism to keep the UI and UX consistent when the native effect isn't there.

The same 'Add debt' screen running on an iPhone 17 Pro Max simulator: a native Liquid Glass close button in the orange header, borrow date and term shown as pill buttons, and a native translucent calendar popover with 8 February 2026 selected in orange
The build on an iPhone 17 Pro Max simulator: native glass button and calendar, rendered by SwiftUI.

Conclusion

The goal is to bring native UI primitives into our own React Native repo.

Expo supports every direction here, because you can pick the right tool for each part of the app:

In its current version, Expo UI focuses on SwiftUI first; Jetpack Compose support for Android is coming in later updates.

The Google Apps Script logo: four rounded bars in red, yellow, green and blue fanned out on a light grey background

Article · October 2026 · 8 min

A free cron job for side projects, with Google Apps Script

No server, no paid plan. How I use a Google Apps Script time-driven trigger as a cron job that tells my backend to send overdue debt reminder emails.

Read article