| name | mobile-ui-patterns |
| description | Canonical mobile UI patterns (list, detail, tab, sheet, master-detail, search, form) with platform-aware guidance across SwiftUI, Compose, Flutter, and React Native. Use when designing or reviewing screens. |
Mobile UI Patterns
Instructions
Mobile apps are built from a small set of recurring UI patterns. Using canonical patterns reduces design debt, makes navigation predictable, and keeps the codebase learnable. This skill names the patterns and shows the cross-platform equivalents.
1. The Core Pattern Set
| Pattern | When to use | Platform convention cue |
|---|
| List | Browsing a homogeneous collection | Plain list, grouped list (iOS), LazyColumn (Android) |
| Detail | Focused view of a single item | Pushed onto the stack from a list |
| Tabs | 2-5 peer destinations always available | Bottom tab bar (both platforms) |
| Sheet | Secondary task or disclosure | Modal/half sheet (iOS 15+), BottomSheet (Android) |
| Master-detail | Large screens: list on left, detail on right | iPad, foldable, tablet layouts |
| Search | Filtering or discovery | searchable (SwiftUI), SearchBar (Compose) |
| Form | Structured data entry | Grouped list style; keyboard-aware |
| Feed | Vertical infinite content | Pull-to-refresh + pagination |
| Onboarding | First-run explanation | Horizontal paged, skippable |
| Empty / error / loading | Non-happy path of any screen | Tri-state render contract |
2. The Tri-State Screen Contract
Every data-driven screen must handle loading, empty, and error as first-class states alongside content. Agents should reject "success-only" designs.
switch viewModel.state {
case .loading: ProgressView()
case .empty: EmptyStateView(onAction: viewModel.createNew)
case .error(let e): ErrorStateView(message: e.localizedDescription, retry: viewModel.load)
case .content(let items): List(items) { ItemRow(item: $0) }
}
when (val s = state) {
UiState.Loading -> CircularProgressIndicator()
UiState.Empty -> EmptyState(onAction = vm::createNew)
is UiState.Error -> ErrorState(s.message, retry = vm::load)
is UiState.Content -> LazyColumn { items(s.items) { ItemRow(it) } }
}
// Flutter
return switch (state) {
Loading() => const Center(child: CircularProgressIndicator()),
Empty() => EmptyState(onAction: vm.createNew),
Error(:final message)=> ErrorState(message: message, retry: vm.load),
Content(:final items)=> ListView(children: items.map(ItemRow.new).toList()),
};
if (state.kind === 'loading') return <ActivityIndicator />;
if (state.kind === 'empty') return <EmptyState onAction={vm.createNew} />;
if (state.kind === 'error') return <ErrorState message={state.message} retry={vm.load} />;
return <FlatList data={state.items} renderItem={({item}) => <ItemRow item={item} />} />;
3. List Pattern Details
- Use lazy containers:
List, LazyColumn, ListView.builder, FlatList. Never map over an array of 1000 items into a non-virtualized container.
- Row height should be stable. If not, supply a key/id to aid diffing.
- Pull-to-refresh is expected on any user-owned list.
- Pagination is either cursor-based (preferred) or page-index-based, never both.
- Separator style follows platform: iOS inset separators, Android divider or none with spacing.
4. Detail Pattern Details
- Hero item is above the fold on phones.
- Primary action is a single, visually dominant button.
- Destructive actions go under an overflow menu or a confirmation sheet, never a primary button.
- On iOS, large navigation title collapses on scroll; on Android, use collapsing top app bar sparingly.
5. Tabs Pattern Details
- 2-5 tabs. Six is a code smell; replace with "More".
- Each tab owns its own navigation stack. Deep links restore into the right tab.
- Tab icons use filled/outlined variants for selected/unselected on both platforms.
- Do not hide tabs on scroll unless the screen is immersive (video, map).
6. Sheet Pattern Details
- Use sheets for tasks that could return the user to the previous context (composing, filtering).
- iOS 15+ supports
presentationDetents([.medium, .large]); Android uses ModalBottomSheet.
- Avoid stacking sheets more than two deep.
- Sheets are dismissable with a swipe on both platforms; always persist draft state on dismiss.
7. Master-Detail Pattern Details
- Trigger breakpoint at 600-700 dp width (roughly iPad portrait, large foldable unfolded).
- On phones, this collapses to list -> detail push navigation.
- Preserve selection across orientation changes.
- Detail pane must function standalone (bookmarks, deep links land there directly).
8. Search Pattern Details
- Debounce input by 250-400 ms before querying.
- Cancel the previous request when a new one starts.
- Empty state shows recent searches or a helpful prompt.
- Respect the platform: search bar at top on Android (Material), inside navigation on iOS.
9. Form Pattern Details
- Keyboard type matches the field (
.emailAddress, .numberPad, KeyboardType.Password, inputType, keyboardType="email-address").
- Focus advances on return; submit on the final field.
- Validate on blur, not on every keystroke.
- Show a single inline error per field, not a top-of-form summary alone.
- Forms must survive backgrounding and rotation without data loss.
10. Platform Convention Respect
Do not build a Material-styled app on iOS or vice versa. Conventions cost little at build time and dominate perceived quality. When using Flutter or React Native, use adaptive components (CupertinoSwitch / Switch, Platform.select) for controls that have strong platform semantics.
Checklist