💎

React NativeでLiquid Glassに対応する

に公開

こんにちは!テラーノベルでフロントエンドを担当している@kazutoyoです!

こちらはReact Native Advent Calendar 2025の6日目の投稿です。

https://qiita.com/advent-calendar/2025/react-native

昨日の@JunichiHashimotoさんの「Gemini3を活用して個人アプリをExpo53からExpo54に移行してLiquid glassに対応してみた!」に続き、React Native(Expo)でLiquid Glassを適用する方法について紹介します。

Liquid Glassについて

2025年、Appleは次期iOS 26で「Liquid Glass」という新しいデザイン言語を導入しました。Liquid Glassは流体と光の屈折という現実の自然界をモチーフとし、コンテンツへの集中を高め、様々なデバイス・プラットフォーム間で統一したUIを実現することを目的としています。

https://www.apple.com/jp/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/

今回の記事では、React Nativeを使ったアプリ開発において、このLiquid Glassにどのように対応できるのか、実際のコード例を交えながら解説しようと思います。

Liquid Glassとは?その目的

Liquid Glassは、Appleが提唱する新しいデザインパラダイムです。主な目的は以下の3点です

  1. コンテンツへの集中を高める:視覚的なノイズを減らし、ユーザーが本当に見たいコンテンツに集中できるようにする
  2. 統一されたUI体験:様々なデバイスやプラットフォーム間で一貫性のあるUIを提供する
  3. 直感的なデザイン:流体と光の屈折という自然界のメタファーを使い、視覚的に分かりやすく直感的なインターフェースを実現する

ただし、重要な注意点として、過度にLiquid Glassの表現を取り入れてコンテンツに集中できないようにすることは本末転倒です。あくまでコンテンツファーストの考え方を忘れないようにしましょう。

https://developer.apple.com/jp/videos/play/wwdc2025/219/

https://developer.apple.com/jp/videos/play/wwdc2025/356/

React NativeでLiquid Glassに対応する方法

React NativeはiOS/AndroidのネイティブのUIを表示することができます。
そのため、仕組み上はReact NativeでもLiquid Glassに対応できます。

ただし、ネイティブの機能をラップしている関係上、まだReact Nativeで対応できていない部分もいくつかあるので注意が必要です。

サンプルアプリについて

今回の記事では、Liquid Glassに対応したTodoアプリを実装し、GitHubで公開しています。
以下の実装例は、このサンプルアプリのコードを元に解説しています。

https://github.com/kazutoyo/liquid-glass-todo-sample

実際に動作するコードを確認したい方は、ぜひリポジトリをご覧ください。

動作環境

サンプルアプリで使用しているライブラリのバージョンは以下の通りです

  • Expo SDK: 55.0.0
  • Expo Router: 7.0.x(~55.0.4
  • React Native: 0.83.2
  • React Native Screens: 4.23.0
  • expo-glass-effect: 55.0.7
  • @expo/ui: 55.0.1(SwiftUI BETA / Jetpack Compose BETA)
  • React: 19.2.0

次から、実際に各要素をどのように対応するか見ていきましょう。


タブバーの対応

Expo Router(SDK 55)では、Native Tabsがサポートされています(現時点ではAlphaステータス)。

https://docs.expo.dev/router/advanced/native-tabs/

このNative Tabsを利用することで、Liquid Glassに対応したタブバーを簡単に実装できます。

基本的な実装方法

expo-router/unstable-native-tabsをインポートして、以下のように実装します。

app/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon sf="house.fill" md="home" />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Icon sf="gear" md="settings" />
        <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

iOSではsfプロパティでSF Symbolsアイコンを、AndroidではmdプロパティでMaterial Symbolsアイコンを指定できます。

バッジの表示

タブにバッジを表示したい場合は、NativeTabs.Trigger.Badgeコンポーネントを追加するだけです。

app/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon sf="house.fill" md="home" />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Icon sf="gear" md="settings" />
        <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

検索タブの独立表示

iOS 26では、検索タブを独立したタブとして表示できる機能が追加されました。

<NativeTabs.Trigger>role="search"を設定することで、検索タブが独立して表示されます。

app/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="search" role="search">
        <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

minimizeBehavior(タブバーの最小化)

minimizeBehaviorプロパティを使うと、ミュージックアプリのようにスクロール時にタブバーを自動的に最小化する動作を制御できます(iOS 26以上)。

app/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs minimizeBehavior="onScrollDown">
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="tab-1">
        <NativeTabs.Trigger.Label>Tab 1</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

minimizeBehaviorに指定できる値は以下の通りです:

  • automatic(デフォルト):システムのデフォルト動作
  • never:最小化しない
  • onScrollDown:下スクロール時に最小化、上スクロールで展開
  • onScrollUp:上スクロール時に最小化、下スクロールで展開

Bottom Accessory

ミュージックアプリのような、タブの上にフローティングし、スクロールしたときに最小化してタブの右側に表示されるViewは「Bottom Accessory」と呼ばれます。

NativeTabs.BottomAccessoryコンポーネントをNativeTabsの中に追加することで実装できます(iOS 26以上)。

Bottom Accessoryは2つのplacementをとります:

  • regular:タブバーの上の通常位置
  • inline:タブバーが最小化されたときのインライン表示

usePlacementフックを使うと、現在のplacementに応じてUIを切り替えられます。

app/_layout.tsx
import { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { NativeTabs, usePlacement } from 'expo-router/unstable-native-tabs';

function MiniPlayer({
  isPlaying,
  onToggle,
}: {
  isPlaying: boolean;
  onToggle: () => void;
}) {
  const placement = usePlacement();

  return (
    <View style={placement === 'inline' ? styles.inlinePlayer : styles.regularPlayer}>
      <Text>Now Playing...</Text>
      <Pressable onPress={onToggle}>
        <Text>{isPlaying ? 'Pause' : 'Play'}</Text>
      </Pressable>
    </View>
  );
}

export default function TabLayout() {
  // 状態はBottomAccessoryの外側で管理する
  const [isPlaying, setIsPlaying] = useState(false);

  return (
    <NativeTabs>
      <NativeTabs.BottomAccessory>
        <MiniPlayer isPlaying={isPlaying} onToggle={() => setIsPlaying(!isPlaying)} />
      </NativeTabs.BottomAccessory>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="library">
        <NativeTabs.Trigger.Label>Library</NativeTabs.Trigger.Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

const styles = StyleSheet.create({
  inlinePlayer: {
    padding: 8,
  },
  regularPlayer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: 16,
  },
});

iOS 26のナビゲーションバーでは、右側のボタンがLiquid Glassのスタイルで表示されるようになりました。

Stack.Toolbar API

Stack.Toolbarを使うことで、ナビゲーションバーやボトムツールバーにネイティブなボタンやメニューを宣言的に配置できます(Alphaステータス)。

  • placement="right" / placement="left":ヘッダーの右・左に配置
  • placement="bottom"(デフォルト):iOSのみ対応のボトムツールバーを表示

ヘッダーへのボタン追加

app/screen.tsx
import { useState } from 'react';
import { Stack } from 'expo-router';
import { View, Text, Alert } from 'react-native';

export default function NoteScreen() {
  const [isFavorite, setIsFavorite] = useState(false);

  return (
    <>
      <Stack.Toolbar placement="right">
        <Stack.Toolbar.Button
          icon={isFavorite ? 'star.fill' : 'star'}
          onPress={() => setIsFavorite(!isFavorite)}
        />
        <Stack.Toolbar.Button
          icon="square.and.arrow.up"
          onPress={() => Alert.alert('Share')}
        />
      </Stack.Toolbar>

      <View style={{ flex: 1, padding: 16 }}>
        <Text>Screen content...</Text>
      </View>
    </>
  );
}

アクションメニューの追加

複数のアクションをまとめたいときはStack.Toolbar.Menuでドロップダウンメニューを構築できます。

コード
app/screen.tsx
import { Stack } from 'expo-router';
import { Alert } from 'react-native';

export default function Screen() {
  return (
    <>
      <Stack.Toolbar placement="right">
        <Stack.Toolbar.Menu icon="ellipsis.circle">
          <Stack.Toolbar.MenuAction
            icon="pencil"
            onPress={() => Alert.alert('Edit')}>
            Edit
          </Stack.Toolbar.MenuAction>
          <Stack.Toolbar.MenuAction
            icon="trash"
            destructive
            onPress={() => Alert.alert('Delete')}>
            Delete
          </Stack.Toolbar.MenuAction>
          {/* ネストしたサブメニューも可能 */}
          <Stack.Toolbar.Menu title="More">
            <Stack.Toolbar.MenuAction onPress={() => {}}>
              Share
            </Stack.Toolbar.MenuAction>
          </Stack.Toolbar.Menu>
        </Stack.Toolbar.Menu>
      </Stack.Toolbar>
      {/* Screen content */}
    </>
  );
}

ボトムツールバー

iOS標準のボトムツールバーもStack.Toolbarで簡単に実装できます。

app/photos/index.tsx
import { Stack } from 'expo-router';
import { Alert } from 'react-native';

export default function PhotosScreen() {
  return (
    <>
      <Stack.Toolbar>
        <Stack.Toolbar.Button icon="photo.on.rectangle" onPress={() => Alert.alert('Select')}>
          Select
        </Stack.Toolbar.Button>
        <Stack.Toolbar.Spacer />
        <Stack.Toolbar.Button icon="plus" onPress={() => Alert.alert('Add')}>
          Add
        </Stack.Toolbar.Button>
      </Stack.Toolbar>
    </>
  );
}

Expo UIを使ったPrimitive UI

@expo/uiパッケージを使用することで、SwiftUIベースのネイティブUIコンポーネントを実装できます。

https://docs.expo.dev/versions/latest/sdk/ui/

例えば、コンテキストメニューは以下のように実装します。

components/ContextMenuExample.tsx
import { ContextMenu, Host, Button } from '@expo/ui/swift-ui';

export default function ContextMenuExample() {
  return (
    <Host style={{ width: 150, height: 50 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Button
            systemImage="person.crop.circle.badge.xmark"
            onPress={() => console.log('Pressed1')}>
            Hello
          </Button>
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Button variant="bordered">
            Show Menu
          </Button>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}

@expo/uiを使うことで、iOSのネイティブコンポーネントをReact Nativeから直接利用でき、Liquid Glassの恩恵を受けることができます。


カスタムViewでのLiquid Glass効果

expo-glass-effectパッケージを使用することで、カスタムViewにLiquid Glassのガラス効果を適用できます。

https://docs.expo.dev/versions/latest/sdk/glass-effect/

components/GlassExample.tsx
import { GlassView, GlassContainer } from 'expo-glass-effect';
import { StyleSheet } from 'react-native';

export const GlassContainerDemo = () => {
  return (
    <GlassContainer spacing={10} style={styles.containerStyle}>
      <GlassView style={styles.glass1} isInteractive />
      <GlassView style={styles.glass2} />
      <GlassView style={styles.glass3} />
    </GlassContainer>
  );
};

const styles = StyleSheet.create({
  containerStyle: {
    flex: 1,
    padding: 20,
    gap: 10,
  },
  glass1: {
    height: 100,
    borderRadius: 12,
  },
  glass2: {
    height: 100,
    borderRadius: 12,
  },
  glass3: {
    height: 100,
    borderRadius: 12,
  },
});

GlassContainerGlassViewの親Viewとして使用し、spacingプロパティでGlassView同士が接するときの融合効果を設定できます。


React NativeでのLiquid Glass対応について

実際にReact NativeでLiquid Glassに対応してみて、以下のようなメリットと課題が見えてきました。

メリット

ネイティブなUI体験の実現

  • Expo UIなどを活用することで、SwiftUIの機能をReact Nativeでも利用できる
  • iOS標準のUIパターンを簡単に実装できる

既存のExpo環境との親和性

  • Expo Routerを使っている場合、比較的少ない変更で対応可能
  • 段階的な導入がしやすい

課題と考慮点

1. APIの安定性

多くの機能がunstable_プレフィックス付き、またはAlpha/Betaで提供されており、APIがまだ安定していません。将来的に破壊的変更が入る可能性があるため、使用には注意が必要です。

2. 既存デザインとの整合性

既存のアプリにLiquid Glassを適用する場合、既存のデザインシステムとの調和を慎重に検討する必要があります

  • Bottom Tabsを採用できるか
  • コンポーネントの角丸をiOS向けに調整するか
  • ボタンやカードのスタイルをどこまでLiquid Glassに寄せるか

3. プラットフォーム間の分岐

iOSとAndroidで実装を分ける必要が出てきます。Stack.ToolbarはiOS専用です。Expo UIを使う場合も、完全に異なる実装が必要になることがあります。また、iOS 26以上でしか使えない機能を、それ以外の環境でどうフォールバックするかも考える必要があります。

実装の判断基準

Liquid Glassへの対応を検討する際は、以下のステップで進めることをおすすめします

  1. メリットの洗い出し:Liquid Glassに対応することで、ユーザー体験がどう向上するか具体的に検討する
  2. 対応範囲の決定:すべてに対応するのではなく、効果の高い部分から段階的に対応する
  3. フォールバック戦略:iOS 26未満やAndroidでどう表示するかを事前に決めておく
  4. メンテナンスコスト:分岐が増えることによるコード複雑性の増加を許容できるか検討する

まとめ

React Nativeでも、Expo RouterやExpo SDKの各種ライブラリを活用することで、iOS 26のLiquid Glassに対応することができます。

ただし、現時点ではまだ一部のAPIがAlpha/Betaで安定していないため、慎重に対応範囲を検討する必要があります。

細かな調整をするほどプラットフォーム間の分岐も増えるため、どの程度対応していくかは、アプリの特性やターゲットユーザー、開発リソースを考慮して判断しましょう。

まずはLiquid Glassに対応することでどのようなメリットがあるかを十分に検討し、何を対応すべきか優先順位をつけてから段階的に実装していくのが良いアプローチだと思います。

それでは、よいReact Nativeライフを!

参考文献


こちらの記事は、React Native Meetup #23での発表を元に作成しています。

https://rnm-23.kazutoyo.jp/

テラーノベル テックブログ

Discussion