iPhoneのノッチやDynamic Island、Androidのステータスバーやナビゲーションバー。デバイスごとに異なる「表示が遮られる領域」をアプリ側で正確に把握しなければ、UIの一部がユーザーに見えなくなります。react-native-safe-area-contextが提供するuseSafeAreaInsetsフックは、この問題をJavaScriptレイヤーから柔軟に解決する手段です。

セーフエリア(Safe Area)の概要

セーフエリアとは、端末のハードウェア要素やシステムUIに遮られず、アプリのコンテンツを安全に表示できる画面領域です。iPhoneではノッチ・Dynamic Island・ホームインジケーターが該当し、Androidではステータスバー・ナビゲーションバー・ディスプレイカットアウト(パンチホールカメラなど)がセーフエリア外に位置します。

これらの領域サイズはデバイスごとに異なるため、ハードコーディングでは対応できません。OSが提供するセーフエリア情報をランタイムに取得し、レイアウトに反映する仕組みが必要です。

useSafeAreaInsetsフックの基礎

インストールと初期設定

useSafeAreaInsetsreact-native-safe-area-contextライブラリに含まれるReact Hooksです。2026年2月時点の最新バージョンはv5.6.2で、iOS・Android・Web・macOS・Windowsをサポートしています。

Expoプロジェクトの場合:

npx expo install react-native-safe-area-context

素のReact Nativeプロジェクトの場合:

npm install react-native-safe-area-context
# iOS
cd ios && pod install

フックを利用するには、アプリのルートコンポーネントをSafeAreaProviderで囲む必要があります。

import { SafeAreaProvider } from 'react-native-safe-area-context';

export default function App() {
  return (
    <SafeAreaProvider>
      {/* アプリ全体のコンポーネントツリー */}
    </SafeAreaProvider>
  );
}

SafeAreaProviderはReact Contextを通じてセーフエリアの値を配下のコンポーネントへ伝搬します。react-native-screensを使用している場合、モーダルやルートコンポーネントごとにもSafeAreaProviderを配置してください。

戻り値(EdgeInsets)の構造

useSafeAreaInsets()EdgeInsetsオブジェクトを返します。

const insets = useSafeAreaInsets();
// insets.top: number    - 上端のインセット(ステータスバー・ノッチ領域)
// insets.bottom: number - 下端のインセット(ホームインジケーター領域)
// insets.left: number   - 左端のインセット(横画面時のノッチ領域など)
// insets.right: number  - 右端のインセット(横画面時のノッチ領域など)

各プロパティの値はピクセル単位(dp)で、デバイスの物理的な遮蔽領域の大きさを表します。縦画面のiPhoneではtopbottomに値が入り、leftrightは0になるのが一般的です。横画面に回転するとleftrightにもノッチ分の値が反映されます。

値の更新は非同期で行われるため、画面回転時にはわずかな遅延が生じる点に注意してください。

基本的な使い方

import { View, Text, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

function HomeScreen() {
  const insets = useSafeAreaInsets();

  return (
    <View style={[styles.container, {
      paddingTop: insets.top,
      paddingBottom: insets.bottom,
      paddingLeft: insets.left,
      paddingRight: insets.right,
    }]}>
      <Text>コンテンツがセーフエリア内に表示されます</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
});

Math.maxを使えば、セーフエリアと最小パディングのいずれか大きい方を適用できます。

<View style={{ paddingBottom: Math.max(insets.bottom, 16) }}>
  <Text>最低16dpのパディングを保証</Text>
</View>

SafeAreaViewとuseSafeAreaInsetsの比較

react-native-safe-area-contextはセーフエリアを扱うための手段を複数提供しています。目的によって適切な手段が異なります。

観点SafeAreaViewuseSafeAreaInsetsSafeAreaInsetsContext.Consumer
利用形態コンポーネントReact HooksRender Props
適用方法自動でpaddingを付与手動でstyleに反映手動でstyleに反映
辺の指定edgesプロップで選択可JavaScript側で自由に制御JavaScript側で自由に制御
レイアウト自由度低(padding固定)高(margin・position等も可)
回転時の挙動ネイティブ側で即時反映非同期更新のためわずかに遅延非同期更新のためわずかに遅延
クラスコンポーネント利用可利用不可利用可
主な用途画面全体のラッピングヘッダー・フッター等の部分適用クラスコンポーネントでの利用

SafeAreaViewを選ぶ場面: 画面全体をセーフエリアで囲むだけで十分なケース。edgesプロップで['top', 'bottom']のように適用する辺を限定できます。回転時のパフォーマンスが優先される場合にも有利です。

useSafeAreaInsetsを選ぶ場面: インセット値をJavaScriptで加工して使いたいケース。ヘッダーの高さにtop insetsを加算する、ボトムタブの高さにbottom insetsを足すなど、きめ細かなレイアウト制御が可能です。React Navigationの公式ドキュメントでも、画面遷移のアニメーション時にちらつきを防ぐためフックの利用が推奨されています。

実装パターン集

カスタムヘッダーに適用する

ナビゲーションのデフォルトヘッダーを使わず、独自のヘッダーを実装する場合はinsets.topを高さに加算します。

import { View, Text, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

function CustomHeader({ title }: { title: string }) {
  const insets = useSafeAreaInsets();
  const HEADER_HEIGHT = 56;

  return (
    <View style={[styles.header, {
      paddingTop: insets.top,
      height: HEADER_HEIGHT + insets.top,
    }]}>
      <Text style={styles.headerTitle}>{title}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  header: {
    backgroundColor: '#6200ee',
    justifyContent: 'center',
    alignItems: 'center',
  },
  headerTitle: {
    color: '#ffffff',
    fontSize: 18,
    fontWeight: 'bold',
  },
});

ボトムタブ・フローティングボタンへの適用

画面下部に固定配置する要素にはinsets.bottomを反映します。

function BottomActions() {
  const insets = useSafeAreaInsets();

  return (
    <View style={[styles.bottomBar, {
      paddingBottom: Math.max(insets.bottom, 12),
    }]}>
      <TouchableOpacity style={styles.actionButton}>
        <Text style={styles.buttonText}>送信</Text>
      </TouchableOpacity>
    </View>
  );
}

ホームインジケーターが存在しないAndroid端末ではinsets.bottomが0になるため、Math.maxで最小値を設定しておくとプラットフォーム間でUIの一貫性を保てます。

スクロール画面での活用

FlatListScrollViewで全画面コンテンツを表示する場合、contentContainerStyleにインセットを反映します。

import { FlatList } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

function ItemList({ data }: { data: Item[] }) {
  const insets = useSafeAreaInsets();

  return (
    <FlatList
      data={data}
      renderItem={({ item }) => <ItemRow item={item} />}
      keyExtractor={(item) => item.id}
      contentContainerStyle={{
        paddingTop: insets.top,
        paddingBottom: insets.bottom,
      }}
    />
  );
}

FlatListSafeAreaViewの中に入れてさらにScrollViewでラップすると、「VirtualizedLists should never be nested inside plain ScrollViews」という警告が出ます。useSafeAreaInsetscontentContainerStyleに直接paddingを指定すれば、この問題を回避できます。

横画面(ランドスケープ)での対応

横画面ではノッチが左右のいずれかに位置するため、insets.leftinsets.rightの両方を考慮します。

function LandscapeContent() {
  const insets = useSafeAreaInsets();

  return (
    <View style={{
      flex: 1,
      paddingTop: insets.top,
      paddingBottom: insets.bottom,
      paddingLeft: Math.max(insets.left, 16),
      paddingRight: Math.max(insets.right, 16),
    }}>
      <Text>横画面でもコンテンツが遮られません</Text>
    </View>
  );
}

端末を回転させるとインセット値が非同期に更新されます。レイアウトの切り替え時にちらつきが気になる場合は、回転アニメーション中にコンテンツをフェードさせるといった工夫が有効です。

ExpoおよびExpo Routerでの導入

Expo SDKを利用するプロジェクトではreact-native-safe-area-contextがpeer dependencyとして含まれていることが多く、Expo Routerを使っている場合は自動的にSafeAreaProviderがセットアップされます。

Expo Router環境での確認ポイント:

  • Expo RouterはルートレイアウトにSafeAreaProviderを自動で組み込みます。手動でラップする必要はありません
  • 個別画面でセーフエリアを制御したい場合は、各画面コンポーネント内でuseSafeAreaInsetsを呼び出します
// app/_layout.tsx(Expo Router利用時)
import { Stack } from 'expo-router';

export default function RootLayout() {
  // SafeAreaProviderはExpo Routerが自動提供
  return <Stack />;
}

// app/index.tsx
import { View, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function HomeScreen() {
  const insets = useSafeAreaInsets();

  return (
    <View style={{ flex: 1, paddingTop: insets.top }}>
      <Text>Expo Routerプロジェクトの画面</Text>
    </View>
  );
}

Expo Routerを使わないExpoプロジェクトでは、App.tsxSafeAreaProviderを手動で配置してください。

React Navigationとの連携

React Navigationはデフォルトでヘッダーやタブバーにセーフエリアを自動適用します。しかし、以下のケースではuseSafeAreaInsetsによる手動対応が必要です。

  • headerShown: falseでヘッダーを非表示にしている画面
  • tabBarを独自コンポーネントに置き換えている場合
  • presentation: 'modal'で表示する画面のコンテンツ部分
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';

const Stack = createNativeStackNavigator();

function FullScreenPage() {
  const insets = useSafeAreaInsets();

  return (
    <View style={{
      flex: 1,
      paddingTop: insets.top,
      paddingBottom: insets.bottom,
    }}>
      <Text>ヘッダーなしの全画面ページ</Text>
    </View>
  );
}

export default function App() {
  return (
    <SafeAreaProvider>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen
            name="FullScreen"
            component={FullScreenPage}
            options={{ headerShown: false }}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </SafeAreaProvider>
  );
}

React Navigationの公式ドキュメントでは、SafeAreaViewコンポーネントよりもuseSafeAreaInsetsフックの利用が推奨されています。画面遷移アニメーション中のレイアウトのちらつきを防げるためです。

iOS・Androidのプラットフォーム差異

セーフエリアのインセット値はプラットフォームとデバイスによって大きく異なります。

デバイス例topbottomleftright
iPhone 16 Pro(縦画面)623400
iPhone SE 3(縦画面)20000
Android(ステータスバーあり・ナビゲーションバーあり)24〜484800
Android(ジェスチャーナビゲーション)24〜480〜2000

iOSの特徴:

  • ノッチ/Dynamic Island搭載機ではtopが44〜62程度
  • ホームインジケーター搭載機ではbottomが34程度
  • ホームボタン搭載機(iPhone SE等)ではtopが20、bottomが0

Androidの特徴:

  • ステータスバーの高さはメーカー・OSバージョンにより異なる
  • Android 15以降はedge-to-edge表示がデフォルトとなり、セーフエリア対応の重要性が増している
  • 3ボタンナビゲーションとジェスチャーナビゲーションでbottomの値が変わる

useSafeAreaInsetsはこれらの差異をランタイムに吸収するため、プラットフォームごとの条件分岐を書く必要がありません。

よくあるトラブルと解決策

insetsがすべて0を返す

useSafeAreaInsets{ top: 0, bottom: 0, left: 0, right: 0 }を返す場合、ほとんどがSafeAreaProviderの配置漏れが原因です。

チェックリスト:

  1. アプリのルートにSafeAreaProviderが配置されているか
  2. react-native-screensを使用している場合、モーダルのルートにもSafeAreaProviderを追加しているか
  3. Expo Routerを使っているなら追加不要(自動で提供されます)
  4. テスト環境(Jest等)で実行している場合はinitialWindowMetricsのモック設定が必要

テスト時のモック例:

import { SafeAreaProvider } from 'react-native-safe-area-context';

const mockInsets = {
  frame: { x: 0, y: 0, width: 393, height: 852 },
  insets: { top: 59, bottom: 34, left: 0, right: 0 },
};

function renderWithSafeArea(component: React.ReactElement) {
  return render(
    <SafeAreaProvider initialMetrics={mockInsets}>
      {component}
    </SafeAreaProvider>
  );
}

Android端末で正しく動作しない

Android固有の問題として以下が報告されています。

  • translucent設定の影響: StatusBarコンポーネントでtranslucent={true}を設定していない場合、ステータスバー分の高さがinsetsに含まれないことがあります
  • Android 15のedge-to-edge対応: react-native-safe-area-context v5系で対応済みですが、古いバージョンでは正しい値が取得できない場合があります。v5.6.2以降へのアップデートを推奨します
  • エミュレーターでの差異: 物理デバイスと異なるインセット値が返される場合があります。実機での動作確認が確実です

クラスコンポーネントで利用したい場合

useSafeAreaInsetsはReact Hooksのため、クラスコンポーネントから直接呼び出せません。代わりにSafeAreaInsetsContext.Consumerを使用します。

import { SafeAreaInsetsContext } from 'react-native-safe-area-context';

class LegacyScreen extends React.Component {
  render() {
    return (
      <SafeAreaInsetsContext.Consumer>
        {(insets) => (
          <View style={{
            flex: 1,
            paddingTop: insets?.top ?? 0,
            paddingBottom: insets?.bottom ?? 0,
          }}>
            <Text>クラスコンポーネントでのセーフエリア対応</Text>
          </View>
        )}
      </SafeAreaInsetsContext.Consumer>
    );
  }
}

あるいは、インセット値を注入するHOC(Higher-Order Component)を作成する方法もあります。

import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { EdgeInsets } from 'react-native-safe-area-context';

function withSafeAreaInsets<P extends { insets: EdgeInsets }>(
  WrappedComponent: React.ComponentType<P>
) {
  return function WithInsets(props: Omit<P, 'insets'>) {
    const insets = useSafeAreaInsets();
    return <WrappedComponent {...(props as P)} insets={insets} />;
  };
}

パフォーマンスを高める工夫

initialWindowMetricsで初回描画を高速化

SafeAreaProviderinitialMetricsを渡すと、ネイティブモジュールの初期化を待たずにインセット値を即座に利用できます。初回描画時のレイアウトシフトが軽減されます。

import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context';

export default function App() {
  return (
    <SafeAreaProvider initialMetrics={initialWindowMetrics}>
      {/* アプリ本体 */}
    </SafeAreaProvider>
  );
}

initialWindowMetricsはネイティブ側でアプリ起動時に計測された値です。Web環境やSSR環境ではnullになるため、サーバーサイドレンダリング時にはフォールバック値の設定が必要です。

不要な再レンダリングを避ける

useSafeAreaInsetsはインセット値が変化するたびに再レンダリングを発生させます。通常の縦画面操作では値が変わることはありませんが、画面回転をサポートするアプリでは注意が必要です。

インセット値を使わない子コンポーネントが不要に再レンダリングされないよう、React.memoやコンポーネント分割を活用してください。

// インセット値を受け取るコンポーネントと、内部のコンテンツを分離
function ScreenWithInsets() {
  const insets = useSafeAreaInsets();
  return (
    <View style={{ flex: 1, paddingTop: insets.top }}>
      <HeavyContent />
    </View>
  );
}

const HeavyContent = React.memo(function HeavyContent() {
  // このコンポーネントはinsetsの変化では再レンダリングされない
  return <ExpensiveList />;
});

まとめ

useSafeAreaInsetsは、React Nativeアプリで各デバイスのセーフエリアをJavaScriptから柔軟に制御できるフックです。SafeAreaViewが画面全体のラッピングに適している一方、フックを使えばヘッダー・フッター・スクロールコンテンツなど部分的な領域に対してインセット値を自由に適用できます。

導入時のポイントを整理します。

  • SafeAreaProviderの配置を忘れない — ルートコンポーネントに1つ、モーダル使用時は各モーダルにも配置
  • Expo Routerなら追加設定不要SafeAreaProviderは自動提供される
  • Math.maxで最小パディングを確保 — プラットフォーム間のUI一貫性を保つ
  • initialWindowMetricsで初回描画を最適化 — レイアウトシフトを軽減
  • React NavigationではSafeAreaViewよりフックを優先 — 画面遷移時のちらつきを防止
  • クラスコンポーネントではSafeAreaInsetsContext.Consumerを使用

公式ドキュメント: