building-native-ui について
Expo アプリでネイティブ UI を構築する開発者向けに、Expo Router のガイドを提供するスキルです。ナビゲーション、スタイル、コンポーネント、アニメーション、プラットフォーム対応、Expo Go と開発ビルドの選択を扱います。
主な機能
- 01Expo Router のルートとナビゲーションパターン
- 02ネイティブコントロールと UI のスタイル指針
- 03アニメーションとジェスチャーのリファレンス
- 04レスポンシブレイアウトとセーフエリアの実践
- 05Expo Go とカスタムビルドの指針
活用シーン
- 01Expo アプリのルート構成
- 02ネイティブナビゲーションとフォームシートの構築
- 03プラットフォームのテーマに合わせた UI 設計
- 04アプリのテストとビルド方法の選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill building-native-uiインストール後、Claude Code で/building-native-uiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlreferences/animations.mdcontrols.mdgradients.mdform-sheet.mdicons.mdroute-structure.mdsearch.mdmedia.mdvisual-effects.mdtoolbar-and-headers.mdstorage.mdtabs.mdzoom-transitions.mdwebgpu-three.md詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
Expo UI ガイドライン
使用するタイミング
Expo Router で美しいアプリを構築するための完全なガイドが必要なときに、このスキルを使用します。基礎、スタイリング、コンポーネント、ナビゲーション、アニメーション、パターン、ネイティブタブを網羅します。
参考資料
必要に応じて、以下のリソースを参照してください:
references/
animations.md Reanimated: entering, exiting, layout, scroll-driven, gestures
controls.md Native iOS: Switch, Slider, SegmentedControl, DateTimePicker, Picker
form-sheet.md Form sheets in expo-router: configuration, footers and background interaction.
gradients.md CSS gradients via experimental_backgroundImage (New Arch only)
icons.md SF Symbols via expo-image (sf: source), names, animations, weights
media.md Camera, audio, video, and file saving
route-structure.md Route conventions, dynamic routes, groups, folder organization
search.md Search bar with headers, useSearch hook, filtering patterns
storage.md SQLite, AsyncStorage, SecureStore
tabs.md NativeTabs, migration from JS tabs, iOS 26 features
toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only)
visual-effects.md Blur (expo-blur) and liquid glass (expo-glass-effect)
webgpu-three.md 3D graphics, games, GPU visualizations with WebGPU and Three.js
zoom-transitions.md Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+)
アプリの実行
重要: カスタムビルドを作成する前に、必ず最初に Expo Go を試してください。
ほとんどの Expo アプリは、カスタムネイティブコードなしで Expo Go で動作します。npx expo run:ios または npx expo run:android を実行する前に:
- Expo Go から始める:
npx expo startを実行し、Expo Go で QR コードをスキャンします - 機能が動作するか確認する: Expo Go でアプリを徹底的にテストします
- カスタムビルドは必要な場合にのみ作成する - 以下を参照してください
カスタムビルドが必要な場合
npx expo run:ios/android または eas build が必要なのは、以下を使用する場合のみです:
- ローカル Expo モジュール (
modules/内のカスタムネイティブコード) - Apple ターゲット (
@bacons/apple-targets経由のウィジェット、App Clip、拡張機能) - Expo Go に含まれていないサードパーティのネイティブモジュール
app.jsonで表現できないカスタムネイティブ設定
Expo Go が動作する場合
Expo Go は、幅広い機能をすぐに利用できる形でサポートしています:
- すべての
expo-*パッケージ (カメラ、位置情報、通知など) - Expo Router ナビゲーション
- ほとんどの UI ライブラリ (reanimated、gesture handler など)
- プッシュ通知、ディープリンクなど
不明な場合は、まず Expo Go を試してください。 カスタムビルドの作成は、複雑さを増し、イテレーションを遅くし、Xcode/Android Studio のセットアップを必要とします。
コードスタイル
- 終端されていない文字列に注意してください。ネストされたバッククォートはエスケープされていることを確認し、引用符を正しくエスケープすることを決して忘れないでください。
- 常にファイルの先頭で import 文を使用してください。
- ファイル名には常に kebab-case を使用してください (例:
comment-card.tsx) - ナビゲーションを移動または再構築するときは、常に古いルートファイルを削除してください
- ファイル名に特殊文字を使用しないでください
- tsconfig.json をパスエイリアスで設定し、リファクタリングでは相対インポートよりもエイリアスを優先してください。
ルート
詳細なルート規約については、./references/route-structure.md を参照してください。
- ルートは
appディレクトリに属します。 - app ディレクトリにコンポーネント、型、ユーティリティを同居させないでください。これはアンチパターンです。
- アプリには常に "/" に一致するルートがあることを確認してください。それはグループルート内にある場合があります。
ライブラリの推奨
- Picker、WebView、SafeAreaView、AsyncStorage など、React Native から削除されたモジュールは使用しないでください
- レガシーの expo-permissions は使用しないでください
expo-avではなくexpo-audioexpo-avではなくexpo-video- SF Symbols には
expo-symbolsや@expo/vector-iconsではなく、source="sf:name"を指定したexpo-image - react-native の SafeAreaView ではなく
react-native-safe-area-context Platform.OSではなくprocess.env.EXPO_OSReact.useContextではなくReact.use- 固有要素
imgではなくexpo-imageの Image コンポーネント - liquid glass の背景には
expo-glass-effect - ネイティブのセマンティックカラーには生の
PlatformColorではなく、expo-routerのColor(型安全で、ライト/ダークに自動適応) - SDK 56 以降では、
@react-navigation/*から直接インポートしないでください — 代わりにexpo-router/react-navigationを使用してください (@react-navigation/native、/core、/elements、/routersをカバーします)
レスポンシブ対応
- レスポンシブ対応のために、常にルートコンポーネントをスクロールビューでラップしてください
- よりスマートなセーフエリアインセットのために、
<SafeAreaView>ではなく<ScrollView contentInsetAdjustmentBehavior="automatic" />を使用してください contentInsetAdjustmentBehavior="automatic"は FlatList と SectionList にも適用してください- Dimensions API ではなく flexbox を使用してください
- 画面サイズを測定するには、
Dimensions.get()よりも常にuseWindowDimensionsを優先してください
動作
- iOS では expo-haptics を条件付きで使用し、より快適な体験を実現します
- React Native の
<Switch />や@react-native-community/datetimepickerなど、ハプティクスを内蔵したビューを使用します - ルートが Stack に属する場合、その最初の子はほぼ常に
contentInsetAdjustmentBehavior="automatic"を設定した ScrollView にするべきです - ページに
ScrollViewを追加する場合、ほぼ常にルートコンポーネント内の最初のコンポーネントにするべきです - 検索を追加する場合は、Stack.Screen のオプションで
headerSearchBarOptionsを優先します - コピーされる可能性のあるデータを含むテキストには
<Text selectable />プロパティを使用します - 1.4M や 38k のような大きな数値のフォーマットを検討します
- webview または Expo DOM コンポーネント内でない限り、'img' や 'div' のような intrinsic 要素は決して使用しないでください
スタイリング
Apple Human Interface Guidelines に従ってください。
一般的なスタイリングルール
- margin や padding スタイルよりも flex gap を優先します
- 可能な場合は margin よりも padding を優先します
- 常にセーフエリアを考慮します。スタックヘッダー、タブ、または ScrollView/FlatList の
contentInsetAdjustmentBehavior="automatic"のいずれかを使用します - 上部と下部の両方のセーフエリアインセットが考慮されていることを確認します
- スタイルの再利用が速い場合を除き、StyleSheet.create ではなくインラインスタイルを使用します
- 状態変化に対して entering および exiting アニメーションを追加します
- カプセル形状を作成する場合を除き、角丸には
{ borderCurve: 'continuous' }を使用します - ページ上のカスタムテキスト要素ではなく、常にナビゲーションスタックのタイトルを使用します
- ScrollView に padding を適用する場合は、ScrollView 自体に padding を適用するのではなく、
contentContainerStyleの padding と gap を使用します (クリッピングが減少します) - CSS と Tailwind はサポートされていません - インラインスタイルを使用してください
カラー
ネイティブのセマンティックカラーには expo-router の Color API を使用します。これは PlatformColor の型安全なラッパーであり、iOS の UIKit カラーを Color.ios.* で、Android の Material 3 カラーを Color.android.material.* (静的) または Color.android.dynamic.* (Android 12 以降でユーザーの壁紙に適応) で公開します。これらはデバイス上で解決され、ライト/ダークモードとアクセシビリティ設定に自動的に適応するため、個別のライト/ダークの hex テーブルや colors.web.ts ファイルを維持する必要がなくなります。
Color はプラットフォーム固有であるため、各値を Platform.select でラップし、web 用の default hex フォールバックを指定します。パレットを theme/colors.ts に集約し、どこでも colors をインポートします:
// theme/colors.ts
import { Platform } from "react-native";
import { Color } from "expo-router";
export const colors = {
label: Platform.select({
ios: Color.ios.label,
android: Color.android.dynamic.onSurface,
default: "#000000",
})!,
secondaryLabel: Platform.select({
ios: Color.ios.secondaryLabel,
android: Color.android.dynamic.onSurfaceVariant,
default: "#3c3c43",
})!,
separator: Platform.select({
ios: Color.ios.separator,
android: Color.android.dynamic.outlineVariant,
default: "#c6c6c8",
})!,
systemBackground: Platform.select({
ios: Color.ios.systemBackground,
android: Color.android.dynamic.surface,
default: "#ffffff",
})!,
systemBlue: Platform.select({
ios: Color.ios.systemBlue,
android: Color.android.dynamic.primary,
default: "#007aff",
})!,
};
import { colors } from "@/theme/colors";
<View style={{ backgroundColor: colors.systemBackground }}>
<Text style={{ color: colors.label }}>Title</Text>
</View>;
- iOS はシステムテーマが変更されると、これらのカラーを自動的に再解決します。Android では、これらをレンダリングする任意のコンポーネント内で
useColorScheme()を呼び出して、テーマが切り替わったときに再レンダリングされるようにします (React Compiler がコンポーネントをメモ化する場合に必要です)。 Color/PlatformColorの値を Reanimated スタイルに渡さないでください — そこでは静的なカラーを使用します (references/animations.mdを参照)。Platform.select({...})!はstring | OpaqueColorValueを返します。ほとんどの React Native スタイルプロパティはColorValue(string | OpaqueColorValue) を受け入れるため、これは問題なく機能します。ただし、一部のサードパーティプロパティはstringのみを受け入れます (例:expo-imageのtintColor)。必要に応じてキャストします:colors.label as string。
テキストスタイリング
- 重要なデータやエラーメッセージを表示するすべての
<Text/>要素にselectableプロパティを追加します - カウンターは整列のために
{ fontVariant: 'tabular-nums' }を使用するべきです
シャドウ
CSS の boxShadow スタイルプロパティを使用します。従来の React Native の shadow や elevation スタイルは決して使用しないでください。
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
'inset' シャドウもサポートされています。
ナビゲーション
Link
ルート間のナビゲーションには 'expo-router' の <Link href="/path" /> を使用します。
import { Link } from 'expo-router';
// Basic link
<Link href="/path" />
// Wrapping custom components
<Link href="/path" asChild>
<Pressable>...</Pressable>
</Link>
可能な限り、iOS の慣習に従うために <Link.Preview> を含めます。ナビゲーションを強化するために、コンテキストメニューとプレビューを頻繁に追加します。
Stack
- スタックを定義するには常に
_layout.tsxファイルを使用します - ネイティブナビゲーションスタックには 'expo-router/stack' の Stack を使用します
ページタイトル
Stack.Screen のオプションでページタイトルを設定します:
<Stack.Screen options={{ title: "Home" }} />
コンテキストメニュー
Link コンポーネントに長押しのコンテキストメニューを追加します:
import { Link } from "expo-router";
<Link href="/settings" asChild>
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Menu>
<Link.MenuAction
title="Share"
icon="square.and.arrow.up"
onPress={handleSharePress}
/>
<Link.MenuAction
title="Block"
icon="nosign"
destructive
onPress={handleBlockPress}
/>
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} />
<Link.MenuAction
title="Delete"
icon="trash"
destructive
onPress={() => {}}
/>
</Link.Menu>
</Link.Menu>
</Link>;
リンクプレビュー
ナビゲーションを強化するために、リンクプレビューを頻繁に使用します:
<Link href="/settings">
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Preview />
</Link>
リンクプレビューはコンテキストメニューと組み合わせて使用できます。
モーダル
画面をモーダルとして表示します:
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
カスタムモーダルコンポーネントを構築するよりも、こちらを優先してください。
シート
画面を動的なフォームシートとして表示します:
<Stack.Screen
name="sheet"
options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" },
}}
/>
contentStyle: { backgroundColor: "transparent" }を使用すると、iOS 26 以降で背景がリキッドグラスになります。
共通のルート構造
各タブ内にタブとスタックを備えた標準的なアプリレイアウト:
app/
_layout.tsx — <NativeTabs />
(index,search)/
_layout.tsx — <Stack />
index.tsx — Main list
search.tsx — Search view
// app/_layout.tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { ThemeProvider, DarkTheme, DefaultTheme } from "expo-router/react-navigation";
import { useColorScheme } from "react-native";
export default function Layout() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<NativeTabs>
<NativeTabs.Trigger name="(index)">
<NativeTabs.Trigger.Icon sf="list.dash" md="list" />
<NativeTabs.Trigger.Label>Items</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search" />
</NativeTabs>
</ThemeProvider>
);
}
両方のタブが共通の画面をプッシュできるように、共有グループルートを作成します:
// app/(index,search)/_layout.tsx
import { Stack } from "expo-router/stack";
import { colors } from "@/theme/colors";
export default function Layout({ segment }) {
const screen = segment.match(/\((.*)\)/)?.[1]!;
const titles: Record<string, string> = { index: "Items", search: "Search" };
return (
<Stack
screenOptions={{
headerTransparent: true,
headerShadowVisible: false,
headerLargeTitleShadowVisible: false,
headerLargeStyle: { backgroundColor: "transparent" },
headerTitleStyle: { color: colors.label },
headerLargeTitle: true,
headerBlurEffect: "none",
headerBackButtonDisplayMode: "minimal",
}}
>
<Stack.Screen name={screen} options={{ title: titles[screen] }} />
<Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} />
</Stack>
);
}
制限事項
- このスキルは、タスクがその上流製品または API の範囲に明確に一致する場合にのみ使用してください。
- 変更を加える前に、コマンド、API の動作、料金、クォータ、認証情報、デプロイへの影響を最新の公式ドキュメントで確認してください。
- 生成された例を、環境固有のテスト、セキュリティレビュー、または破壊的あるいは高コストな操作に対するユーザーの承認の代わりとして扱わないでください。