
Expo Ios Screen Scaffolder
- 69 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
expo-ios-screen-scaffolder is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
Key points
- expo-ios-screen-scaffolder
- AI & Agent Building
- AI-coding skill
Expo Ios Screen Scaffolder by the numbers
- 69 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,786 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill expo-ios-screen-scaffolderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 69 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I helps with ai & agent building tasks during ai-assisted development?
Helps with ai & agent building tasks during AI-assisted development.
Who is it for?
Best when you're working on ai & agent building and need structured help with expo-ios-screen-scaffolder.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks during ai-assisted development, or when expo-ios-screen-scaffolder is a claude code skill for ai & agent building. it helps solo builders move faster with ai-assiste
What you get
Structured output aligned to expo-ios-screen-scaffolder: expo-ios-screen-scaffolder; AI & Agent Building; AI-coding skill.
Files
Expo iOS Screen Scaffolder
Parameterized templates that generate HIG-compliant Expo Router screens. Every template produces a screen that is native-by-construction — native navigation, virtualized lists, safe areas, semantic colors, SF Symbols, haptics, and honest empty states — so new screens start correct instead of being retrofitted. Each generated file cites the expo-ios-hig rules it satisfies.
When to Apply
Use this skill when the user wants to:
- Create or add a new screen, route, or layout in an Expo app for iOS
- Scaffold a list, detail, form, modal sheet, tab bar, or settings screen
- Start a new Expo feature and want the screens to feel native from the first commit
- Bring an existing screen up to the conventions in
references/conventions.md
Available Templates
To scaffold: read the template, substitute the placeholders, and write the result to its route path under app/. Placeholders use single braces (e.g., {ScreenName}); the literal TSX braces stay as-is.
| Template | Generates | Placeholders |
|---|---|---|
| `list-screen.tsx.template` | Large-title list: FlashList + RefreshControl + empty state + bottom safe-area inset | {ScreenName} {Entity} {entity_plural} {Title} {route_path} {sf_symbol} |
| `detail-screen.tsx.template` | Pushed detail: edge-to-edge scroll, semantic colors, system share button | {ScreenName} {Entity} {entity} {entityId} |
| `form-screen.tsx.template` | Create form: keyboard avoidance, configured input, optimistic save + haptics | {ScreenName} {Entity} {entity} |
| `modal-sheet.tsx.template` | formSheet route with detents + grabber and Cancel/Done | {ScreenName} {Title} {route_name} |
| `native-tabs-layout.tsx.template` | Root NativeTabs layout with SF Symbol icons (repeat per tab) | {tab_name} {Tab_label} {tab_sf_symbol} |
| `settings-screen.tsx.template` | Grouped settings: SectionList + platform Switch rows | {ScreenName} {Title} |
Common placeholders:
{ScreenName}— PascalCase component with aScreensuffix, e.g.TrailsScreen{Entity}/{entity}/{entity_plural}— PascalCase, camelCase, and plural forms, e.g.Trail/trail/trails{entityId}— dynamic route param name, matching the file ([trailId].tsx→trailId){Title}— navigation bar title text, e.g.Trails{route_path}/{route_name}— route segment(s) used for navigation{sf_symbol}/{tab_sf_symbol}— SF Symbol name from Apple's SF Symbols app{file_path}— the destination path, written into the header comment
How to Use
1. Pick the template for the screen type. 2. Choose values for its placeholders (see the table above and the header comment inside each template). 3. Substitute and write to the route path, e.g. app/(trails)/index.tsx, app/(trails)/[trailId].tsx. 4. Implement the imported companion (use{Entity}List, create{Entity}, …) — the scaffold owns the view, you own the data layer. 5. Read references/conventions.md for the rules each template enforces and when to deviate.
Setup
config.json is optional. Override on first use if your project differs:
app_dir— Expo Router routes directory (defaultapp)components_dir— shared components directory (defaultcomponents)list_component—FlashList(default) orFlatListfor the list template
Related Skills
- `expo-ios-hig` — the rules these templates follow; each generated file cites them.
- `expo-ios-hig-verify` — run it after scaffolding to confirm the screen stays native.
// {file_path} — {Entity} detail screen
// Generated by expo-ios-screen-scaffolder
//
// Placeholders:
// - {ScreenName}: PascalCase screen component, e.g. TrailDetailScreen
// - {Entity}: PascalCase singular, e.g. Trail
// - {entity}: camelCase singular, e.g. trail
// - {entityId}: route param name, matching the [trailId].tsx file, e.g. trailId
//
// HIG rules applied: layout-edge-to-edge, visual-semantic-colors, native-sf-symbols,
// system-share-sheet, nav-system-back
import { Stack, useLocalSearchParams } from 'expo-router';
import { ScrollView, Text, Pressable, Share, PlatformColor, StyleSheet } from 'react-native';
import { SymbolView } from 'expo-symbols';
import { use{Entity} } from '../hooks/use{Entity}';
export function {ScreenName}() {
const { {entityId} } = useLocalSearchParams<{ {entityId}: string }>();
const { {entity} } = use{Entity}({entityId});
const onShare = () => {
// TODO: replace example.app with your universal-link domain
Share.share({ title: {entity}?.name, url: 'https://example.app/{entity}/' + {entityId} });
};
return (
<>
{/* nav-system-back stays intact — only headerRight is added, never headerLeft */}
<Stack.Screen
options={{
title: {entity}?.name ?? '{Entity}',
// system-share-sheet: present the real activity view
headerRight: () => (
<Pressable onPress={onShare} accessibilityRole="button" accessibilityLabel="Share">
<SymbolView name="square.and.arrow.up" size={22} />
</Pressable>
),
}}
/>
{/* layout-edge-to-edge: content scrolls under the translucent bar */}
<ScrollView contentInsetAdjustmentBehavior="automatic" style={styles.container}>
<Text style={styles.body}>{ {entity}?.description }</Text>
</ScrollView>
</>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: PlatformColor('systemBackground') },
body: { padding: 16, fontSize: 17, color: PlatformColor('label') },
});
// {file_path} — create {Entity} form screen
// Generated by expo-ios-screen-scaffolder
//
// Placeholders:
// - {ScreenName}: PascalCase screen component, e.g. NewTrailScreen
// - {Entity}: PascalCase singular, e.g. Trail
// - {entity}: camelCase singular, e.g. trail
//
// HIG rules applied: layout-keyboard-avoidance, system-keyboard-type,
// motion-optimistic-updates, touch-haptics-on-outcome, native-system-alert
import { router } from 'expo-router';
import {
KeyboardAvoidingView, ScrollView, TextInput, Button, Alert, PlatformColor, StyleSheet,
} from 'react-native';
import * as Haptics from 'expo-haptics';
import { useState } from 'react';
import { create{Entity} } from '../api/{entity}';
export function {ScreenName}() {
const [name, setName] = useState('');
const onSave = async () => {
try {
await create{Entity}({ name });
// touch-haptics-on-outcome: success haptic only when the save succeeds
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
router.back();
} catch {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
// native-system-alert: the platform alert, not a custom modal
Alert.alert('Could not save', 'Check your connection and try again.');
}
};
return (
// layout-keyboard-avoidance: inputs stay above the keyboard on iOS
<KeyboardAvoidingView behavior="padding" style={styles.container}>
<ScrollView keyboardDismissMode="interactive" contentInsetAdjustmentBehavior="automatic">
{/* system-keyboard-type: configure keyboard, autofill, and return key per field */}
<TextInput
style={styles.field}
placeholder="{Entity} name"
value={name}
onChangeText={setName}
autoCapitalize="words"
returnKeyType="done"
/>
<Button title="Save" onPress={onSave} />
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: PlatformColor('systemBackground') },
field: { padding: 16, fontSize: 17, color: PlatformColor('label') },
});
// {file_path} — {Entity} list screen
// Generated by expo-ios-screen-scaffolder
//
// Placeholders:
// - {ScreenName}: PascalCase screen component, e.g. TrailsScreen
// - {Entity}: PascalCase singular entity, e.g. Trail
// - {entity_plural}: camelCase plural, e.g. trails
// - {Title}: navigation bar title, e.g. Trails
// - {route_path}: route segment pushed for a row, e.g. trails
// - {sf_symbol}: SF Symbol for the empty state, e.g. figure.hiking
//
// HIG rules applied: nav-large-titles, motion-virtualized-lists, touch-pull-to-refresh,
// motion-empty-states, layout-content-inset-under-bars, visual-semantic-colors
import { Stack, router } from 'expo-router';
import { RefreshControl, Text, Pressable, PlatformColor, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { SymbolView } from 'expo-symbols';
import { use{Entity}List } from '../hooks/use{Entity}List';
export function {ScreenName}() {
const insets = useSafeAreaInsets();
const { {entity_plural}, isRefreshing, refresh } = use{Entity}List();
return (
<>
{/* nav-large-titles: collapses on scroll and adopts the scroll-edge appearance */}
<Stack.Screen options={{ title: '{Title}', headerLargeTitle: true }} />
<FlashList
data={ {entity_plural} }
keyExtractor={(item) => item.id}
// touch-pull-to-refresh: the native pull-down spinner
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={refresh} />}
// layout-content-inset-under-bars: last row clears the tab bar and home indicator
contentContainerStyle={{ paddingBottom: insets.bottom + 16 }}
contentInsetAdjustmentBehavior="automatic"
renderItem={({ item }) => (
<Pressable style={styles.row} onPress={() => router.push('/{route_path}/' + item.id)}>
<Text style={styles.title}>{item.name}</Text>
</Pressable>
)}
// motion-empty-states: guide the next action instead of a blank screen
ListEmptyComponent={
<Pressable style={styles.empty} onPress={refresh}>
<SymbolView name="{sf_symbol}" size={40} tintColor={PlatformColor('secondaryLabel')} />
<Text style={styles.emptyText}>No {entity_plural} yet</Text>
</Pressable>
}
/>
</>
);
}
const styles = StyleSheet.create({
row: { paddingHorizontal: 16, paddingVertical: 12 },
title: { fontSize: 17, color: PlatformColor('label') },
empty: { alignItems: 'center', paddingTop: 80, gap: 8 },
emptyText: { color: PlatformColor('secondaryLabel') },
});
// {file_path} — {Title} presented as a form sheet
// Generated by expo-ios-screen-scaffolder
//
// Placeholders:
// - {ScreenName}: PascalCase screen component, e.g. FilterTrailsScreen
// - {Title}: navigation bar title, e.g. Filter
// - {route_name}: this route's segment, e.g. filter
//
// Register the route in the parent _layout so it presents as a sheet with detents:
// <Stack.Screen
// name="{route_name}"
// options={{ presentation: 'formSheet', sheetAllowedDetents: [0.5, 1.0], sheetGrabberVisible: true }}
// />
//
// HIG rules applied: nav-sheet-detents, nav-push-vs-present
import { Stack, router } from 'expo-router';
import { ScrollView, Button, PlatformColor, StyleSheet } from 'react-native';
export function {ScreenName}() {
return (
<>
{/* nav-push-vs-present: a self-contained task gets Cancel/Done, not a back chevron */}
<Stack.Screen
options={{
title: '{Title}',
headerLeft: () => <Button title="Cancel" onPress={() => router.back()} />,
headerRight: () => <Button title="Done" onPress={() => router.back()} />,
}}
/>
<ScrollView style={styles.container} contentInsetAdjustmentBehavior="automatic">
{/* sheet content goes here */}
</ScrollView>
</>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: PlatformColor('systemBackground') },
});
// {file_path} — root native tab bar layout
// Generated by expo-ios-screen-scaffolder
//
// Placeholders (repeat the Trigger block once per tab):
// - {tab_name}: route segment for the tab, e.g. trails
// - {Tab_label}: visible label, e.g. Trails
// - {tab_sf_symbol}: SF Symbol for the tab icon, e.g. figure.hiking
//
// Requires Expo Router native tabs (a beta API — pin your Expo SDK). The
// { NativeTabs, Icon, Label } named-import form below targets SDK 54+. On SDK 55+
// the icon may instead use the nested <NativeTabs.Trigger.Icon> form — check your SDK's docs.
//
// HIG rules applied: nav-native-tabs, native-sf-symbols
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
export default function TabsLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="{tab_name}">
<Label>{Tab_label}</Label>
<Icon sf="{tab_sf_symbol}" />
</NativeTabs.Trigger>
{/* Duplicate the Trigger above for each additional tab */}
</NativeTabs>
);
}
// {file_path} — {Title} settings screen
// Generated by expo-ios-screen-scaffolder
//
// Placeholders:
// - {ScreenName}: PascalCase screen component, e.g. SettingsScreen
// - {Title}: navigation bar title, e.g. Settings
//
// HIG rules applied: native-switch-toggle, visual-semantic-colors, nav-large-titles,
// layout-content-inset-under-bars
import { Stack } from 'expo-router';
import { SectionList, View, Text, Switch, PlatformColor, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useState } from 'react';
export function {ScreenName}() {
const insets = useSafeAreaInsets();
const [offlineEnabled, setOfflineEnabled] = useState(false);
return (
<>
<Stack.Screen options={{ title: '{Title}', headerLargeTitle: true }} />
<SectionList
contentInsetAdjustmentBehavior="automatic"
// layout-content-inset-under-bars: last row clears the home indicator
contentContainerStyle={{ paddingBottom: insets.bottom + 16 }}
sections={[{ title: 'General', data: ['offline'] }]}
keyExtractor={(item) => item}
renderSectionHeader={({ section }) => <Text style={styles.header}>{section.title}</Text>}
renderItem={() => (
<View style={styles.row}>
<Text style={styles.label}>Available offline</Text>
{/* native-switch-toggle: platform Switch with correct size, haptic, and a11y trait */}
<Switch value={offlineEnabled} onValueChange={setOfflineEnabled} />
</View>
)}
/>
{/* Add more rows via the section's data, or more sections to the sections array above */}
</>
);
}
const styles = StyleSheet.create({
header: {
paddingHorizontal: 16, paddingTop: 24, paddingBottom: 8,
color: PlatformColor('secondaryLabel'),
},
row: {
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
paddingHorizontal: 16, paddingVertical: 12,
backgroundColor: PlatformColor('secondarySystemGroupedBackground'),
},
label: { fontSize: 17, color: PlatformColor('label') },
});
{
"app_dir": "app",
"components_dir": "components",
"list_component": "FlashList",
"_setup_instructions": {
"app_dir": "Expo Router routes directory where scaffolded screens are written (default: 'app').",
"components_dir": "Directory for shared components the screens import (default: 'components').",
"list_component": "List component used by the list-screen template: 'FlashList' (recommended) or 'FlatList'."
}
}
Gotchas
No known gotchas yet. Append entries here as scaffolding edge cases surface during real use, with dates.
Format:
### <short title>
<what goes wrong and how to avoid it>
Added: YYYY-MM-DD{
"version": "0.1.0",
"organization": "Expo iOS HIG",
"technology": "Expo (React Native) for iOS 26",
"discipline": "extraction",
"type": "scaffolding",
"date": "May 2026",
"abstract": "Parameterized templates that scaffold HIG-compliant Expo Router screens for iOS — list, detail, form, modal sheet, native tabs layout, and settings. Each template is native-by-construction (native navigation, FlashList, safe-area insets, semantic colors via PlatformColor, SF Symbols, haptics, empty states) and cites the expo-ios-hig rules it satisfies, so generated screens pass expo-ios-hig-verify without rework. Output is TSX for Expo Router, not Swift.",
"references": [
"https://docs.expo.dev/router/introduction/",
"https://docs.expo.dev/router/advanced/stack/",
"https://docs.expo.dev/router/advanced/native-tabs/",
"https://shopify.github.io/flash-list/docs/",
"https://developer.apple.com/design/human-interface-guidelines/"
]
}
Conventions
The conventions these templates enforce, and why. Each exists to make generated screens native-by-construction and consistent with the expo-ios-hig rules — so a scaffolded screen passes expo-ios-hig-verify without rework.
File placement: under the Expo Router app/ directory
Screens are routes. A screen file's path is its URL — app/(trails)/[trailId].tsx is the route /trails/:trailId. Why: Expo Router is file-based; placing screens anywhere else means they aren't routable. The app_dir config key overrides the directory name.
Component naming: PascalCase with a Screen suffix
The exported component is TrailsScreen, TrailDetailScreen, NewTrailScreen. Why: Distinguishes route screens from shared components at a glance, and the suffix makes imports unambiguous in a large app/ tree.
Route param names match the file: [trailId].tsx → trailId
The {entityId} placeholder is the param the dynamic route file declares. Why: useLocalSearchParams<{ trailId: string }>() must read the exact key Expo Router put there. A mismatch yields undefined at runtime with no compile error.
Colors come from PlatformColor, never hardcoded hex
Every generated style uses semantic colors (label, secondaryLabel, systemBackground, secondarySystemGroupedBackground). Why: Semantic colors track light/dark mode, increased contrast, and elevation automatically (expo-ios-hig rule visual-semantic-colors). Hardcoded hex is frozen to one appearance. Exception: a brand color that must stay constant — define it explicitly, with a dark variant.
Large titles on root screens, inline titles on detail screens
List and settings templates set headerLargeTitle: true; detail templates use the default inline title. Why: iOS shows a collapsing large title at the root of each tab and a standard title deeper in the stack (nav-large-titles). The large title only collapses when the scroll view is the screen's direct child — the templates keep it so.
Lists are virtualized (FlashList by default)
The list template uses FlashList; the list_component config key can switch it to FlatList. Why: Mapping rows into a ScrollView mounts everything at once and drops frames (motion-virtualized-lists). A virtualized list recycles rows for 60fps over large data. Both FlashList and FlatList support the RefreshControl and ListEmptyComponent the template wires up.
Icons are SF Symbols via expo-symbols
Header actions and empty states use SymbolView name="...". Why: SF Symbols scale with Dynamic Type, match font weight, and look native (native-sf-symbols). Pick names from Apple's SF Symbols app.
Modals get Cancel/Done; pushed screens keep the system back button
The modal-sheet template sets headerLeft/headerRight to Cancel/Done; other screens never replace headerLeft. Why: Replacing headerLeft on a pushed screen disables the swipe-back edge gesture (nav-system-back). On a modal that is correct — modals dismiss with Cancel and swipe-down, not swipe-back (nav-push-vs-present).
Each screen cites the rules it satisfies
Every template header lists the expo-ios-hig rules applied. Why: Makes the generated code self-documenting and lets a reviewer (or the expo-ios-hig-verify skill) trace a decision back to its rationale.
Companion files the user provides
Templates import a data hook (use{Entity}List, use{Entity}) or an API function (create{Entity}). These are intentionally not generated. Why: Data access is app-specific. The scaffold owns the HIG-compliant view; the user owns the data layer. Adjust the import paths (../hooks, ../api) to your project structure.
Related skills
FAQ
What does expo-ios-screen-scaffolder do?
expo-ios-screen-scaffolder is a Claude Code skill for ai & agent building. It helps developers move faster with AI-assisted coding.
When should I use expo-ios-screen-scaffolder?
When you need to helps with ai & agent building tasks during ai-assisted development, or when expo-ios-screen-scaffolder is a claude code skill for ai & agent building. it helps developers move faster with ai-assisted coding.
What are the main capabilities?
expo-ios-screen-scaffolder; AI & Agent Building; AI-coding skill.