"The close button doesn't work." "Tapping the dark backdrop does nothing." "Buttons inside the modal feel completely dead." If you've asked Rork's AI to generate a dialog and shipped it to a real device, you've probably hit at least one of these. The generated code looks perfectly reasonable on the screen, yet the modal becomes a trap the moment a user opens it. Tweaking the prompt rarely fixes the underlying pattern.
Eight times out of ten, the cause boils down to one of five concrete pitfalls. React Native's Modal is convenient, but it punishes anyone who treats it like a Web <dialog>. This guide walks through the landmines I've stepped on repeatedly while shipping Rork apps, and shows the fix for each one.
Why Rork-generated modals tend to lock up
When you describe a dialog in natural language, Rork's AI often emits <Modal> without the transparent prop. The model interprets your prompt with a Web mindset, so it builds a full-screen opaque view that physically blocks the "tap outside to dismiss" area you assumed would exist.
The other root cause is a missing onRequestClose. That handler is what wires the modal to Android's hardware back button. iOS appears to behave fine without it, so if you only test on the iOS simulator the bug never shows up — until the moment someone hands the app to an Android user, who has no choice but to force-quit.
The official docs mention this in passing, but Modal is special: it renders into a separate native window, outside your normal React tree. Once you forget that, every interaction with pointerEvents, KeyboardAvoidingView, or FlatList starts behaving in surprising ways.
There's a third, subtler reason worth flagging: Rork's outputs evolve. The same prompt that gave you a clean dialog last month may emit a slightly different shape today, because the underlying model and template library have moved on. Treat the patterns in this guide as a checklist you run every time an AI gives you a modal, not as a one-off audit you perform on legacy code.
Pitfall 1: Missing transparent and onRequestClose
By far the most common. Here's the minimal Rork output you'll see on first generation:
// ❌ Looks fine on iOS sim — broken on Android, no backdrop dismiss
import { Modal, View, Text, Pressable } from 'react-native';
export function BadModal({ visible, onClose }: { visible: boolean; onClose: () => void }) {
return (
<Modal visible={visible} animationType="fade">
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>The close button may not respond.</Text>
<Pressable onPress={onClose}>
<Text>Close</Text>
</Pressable>
</View>
</Modal>
);
}This compiles, runs, and even closes when you tap "Close" on the iOS simulator. But because transparent is omitted, the modal paints an opaque white background, so there's no "outside" to tap. Without onRequestClose, the Android back button is silently ignored — your only option is to kill the app.
The reliable shape looks like this:
// ✅ Works on both platforms, dismisses on backdrop tap
import { Modal, View, Text, Pressable, StyleSheet } from 'react-native';
export function GoodModal({ visible, onClose }: { visible: boolean; onClose: () => void }) {
return (
<Modal
visible={visible}
transparent={true}
animationType="fade"
onRequestClose={onClose} // Required for Android back button
statusBarTranslucent={true} // Cover the status bar area on Android
>
{/* Backdrop dismiss area */}
<Pressable style={styles.backdrop} onPress={onClose}>
{/* Stop propagation so taps inside the card don't dismiss */}
<Pressable style={styles.content} onPress={(e) => e.stopPropagation()}>
<Text>This modal closes reliably.</Text>
<Pressable onPress={onClose} style={styles.button}>
<Text>Close</Text>
</Pressable>
</Pressable>
</Pressable>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
},
content: {
backgroundColor: '#fff',
padding: 24,
borderRadius: 12,
minWidth: 280,
},
button: { marginTop: 16, padding: 12 },
});A small note worth absorbing: transparent={true} does not just affect the visual background — it changes what kind of native window React Native creates. With transparent={false}, the modal owns the entire window and your touch events on the "outside" are quite literally hitting the modal's own opaque background. With transparent={true}, the window is layered over your existing UI, which is what makes the backdrop pattern possible at all.
Two things matter here. The outer Pressable calls onClose, and the inner Pressable calls e.stopPropagation() to swallow the event. That single pattern gives you the "tap outside to dismiss, tap inside to interact" behavior that users expect, on both iOS and Android.
Pitfall 2: TextInput inside a modal keeps losing focus
This bites you on login dialogs and comment forms. The modal opens, the input is focused, the keyboard appears — and the moment it appears, the modal relayouts, the input is unmounted, and focus disappears.
The culprit is the parent re-rendering. If the modal's visible prop lives on the parent's state, every parent state update causes the modal subtree to re-render, and the TextInput instance is replaced with a new one. Rork tends to generate code in exactly this brittle shape.
// ❌ Every keystroke re-renders the parent, which remounts the TextInput
function Parent() {
const [visible, setVisible] = useState(false);
const [text, setText] = useState('');
return (
<Modal visible={visible} transparent onRequestClose={() => setVisible(false)}>
<TextInput value={text} onChangeText={setText} autoFocus />
</Modal>
);
}The fix is to extract the modal contents into a child component so its state lives below the parent's re-render:
// ✅ Child holds the input state — focus survives across keystrokes
function ModalContent({ onClose }: { onClose: () => void }) {
const [text, setText] = useState('');
return (
<View style={{ padding: 24, backgroundColor: '#fff' }}>
<TextInput
value={text}
onChangeText={setText}
autoFocus
style={{ borderWidth: 1, padding: 8 }}
/>
<Pressable onPress={onClose}><Text>Send</Text></Pressable>
</View>
);
}
function Parent() {
const [visible, setVisible] = useState(false);
return (
<Modal visible={visible} transparent onRequestClose={() => setVisible(false)}>
<ModalContent onClose={() => setVisible(false)} />
</Modal>
);
}The focus problem disappears. When you ask Rork to refactor, say something like "extract the modal contents into a separate component" — that explicit phrasing usually gives you the right structure on the first try.
There's a small but worthwhile follow-up: if you're storing form drafts (chat input, comment text, multi-step forms), keep that state inside the child component too. The natural temptation is to lift it back to the parent so you can preview it elsewhere, but the moment you do, the focus problem returns. If you genuinely need the parent to see the value, debounce the upward sync — every 300ms or on submit — rather than on every keystroke.
Pitfall 3: KeyboardAvoidingView swallows your backdrop taps
You add KeyboardAvoidingView to stop the keyboard from covering your input — and now backdrop dismiss stops working. Classic secondary failure.
KeyboardAvoidingView absolutely-positions an internal view, which breaks the parent-child chain your backdrop Pressable relies on. The keyboard-avoiding view captures the touch before your dismiss handler ever sees it.
The trick is to put KeyboardAvoidingView inside the backdrop, not around it:
// ✅ Backdrop wraps everything, KeyboardAvoidingView is nested
<Modal visible={visible} transparent onRequestClose={onClose}>
<Pressable style={styles.backdrop} onPress={onClose}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ width: '100%', alignItems: 'center' }}
>
<Pressable style={styles.content} onPress={(e) => e.stopPropagation()}>
<TextInput style={styles.input} placeholder="Type a message" />
</Pressable>
</KeyboardAvoidingView>
</Pressable>
</Modal>behavior should be padding on iOS and height on Android in most cases. On Android, if your windowSoftInputMode is already adjustResize (the default for Expo apps), the keyboard usually moves out of the way without KeyboardAvoidingView at all — so consider just dropping it in Android-only flows.
Pitfall 4: Putting a Modal inside FlatList's renderItem
This pattern shows up the moment you build a list with a "View details" button per row.
// ❌ One modal per list item — 100 items means 100 native windows
<FlatList
data={items}
renderItem={({ item }) => (
<View>
<Pressable onPress={() => setOpenId(item.id)}><Text>{item.title}</Text></Pressable>
<Modal visible={openId === item.id} transparent onRequestClose={() => setOpenId(null)}>
{/* This is in the tree N times */}
</Modal>
</View>
)}
/>It works, but each Modal allocates a native window. With a thousand items, the Android scroll noticeably stutters. The right design is one Modal outside the list, with the visible content driven by state:
// ✅ A single modal, content swapped via state
function ListWithDetailModal() {
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
return (
<>
<FlatList
data={items}
renderItem={({ item }) => (
<Pressable onPress={() => setSelectedItem(item)}>
<Text>{item.title}</Text>
</Pressable>
)}
/>
<Modal
visible={selectedItem !== null}
transparent
onRequestClose={() => setSelectedItem(null)}
>
{selectedItem && <DetailContent item={selectedItem} onClose={() => setSelectedItem(null)} />}
</Modal>
</>
);
}Now there's exactly one modal regardless of list length. Tell Rork "lift the modal out of the list and store the selected item in state" and it will rewrite to this shape.
There's a related anti-pattern to watch for: nested gesture-responsive components inside the modal content. If the detail view contains its own swipeable rows or a horizontal carousel, you'll occasionally see the modal's backdrop dismiss fire when a user finishes a swipe near the edge. Wrapping the inner content in a View with pointerEvents="box-only" on the right child is usually enough to claim that zone, but the cleaner fix is to design the detail content with the modal's edges in mind from the start.
Pitfall 5: Nesting modals across platforms
You want to open a confirmation dialog from inside your settings modal. iOS happily nests modals; Android often refuses to render the second one, or breaks back-button handling halfway through.
This isn't a React Native bug — Modal maps onto the native modal mechanism (presentViewController: on iOS, Dialog on Android), and the platform differences leak through.
You have three reliable workarounds. First, use Alert.alert() for confirmations — it bypasses the React tree entirely and avoids nesting. Second, when you really need a custom confirmation modal, close the outer modal first, then open the inner one. Third, reach for a third-party library like react-native-modal, which implements modals as pure React components and sidesteps the platform mismatch.
// ✅ Alert.alert handles confirmation, then closes the parent
import { Alert } from 'react-native';
function SettingsModal({ visible, onClose }: Props) {
const handleReset = () => {
Alert.alert(
'Reset all settings?',
'This action cannot be undone.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Reset', style: 'destructive', onPress: () => { resetSettings(); onClose(); } },
]
);
};
return (
<Modal visible={visible} transparent onRequestClose={onClose}>
{/* ... */}
<Pressable onPress={handleReset}><Text>Reset</Text></Pressable>
</Modal>
);
}If you're shipping to both platforms (and you almost always are), bake "test on a physical Android device after every modal change" into your workflow. The asymmetry between iOS and Android Modal handling is the single biggest source of late-stage bug reports I've seen on Rork apps. A two-minute Android sanity check saves you a stack of emails the next morning.
Alert.alert adopts the OS-standard look, so it doesn't feel out of place visually either. Telling Rork "use Alert.alert for the confirmation dialog" produces the right code on the first generation.
Wrapping up — what to do next
Next time a Rork modal won't close, open the file and check two things first: is transparent={true} set, and is onRequestClose wired up? If either is missing, you're almost certainly looking at one of the patterns above. When you regenerate with Rork, add a single sentence like "always set transparent and onRequestClose, and make backdrop tap dismiss the modal" — the resulting code is dramatically more reliable.
One final habit that has saved me hours: keep a small <DebugModal> test screen in your app — a single screen that mounts each of these patterns side by side. When something breaks in the field, you can switch to that screen on a real Android device in 10 seconds and verify whether the underlying primitive still works. It feels overkill until the first time it tells you the bug is in your business logic, not in Modal itself.
If you want to round out your Rork troubleshooting toolkit, the FlatList blank/empty list debug guide and the pull-to-refresh flicker fix guide cover the next-most-common UI bugs that share roots with modal issues.