How Our AI Understands Complex UI Layouts and Hierarchies
(155 chars):** How RapidNative's AI reasons about complex UI layouts and React Native component hierarchies — from Yoga rules to prompt sections and QuickEdit.
By Riya
4th Jul 2026
Last updated: 4th Jul 2026
Ask any large language model to "build me a fitness dashboard screen for React Native" and you'll get code. Ask it to build one that actually renders — with the right ScrollView at the right depth, SafeAreaView respecting the notch, a FlatList inside a scrollable column that doesn't crash, icons that exist in the imported library, and Tailwind classes that NativeWind can actually resolve on mobile — and most models silently produce invalid layouts.
The tricky part of AI mobile UI generation isn't generating tokens. It's producing a valid component hierarchy that matches how React Native's layout engine actually works. Yoga (React Native's flexbox implementation) is stricter than the web. Icons must be pre-imported from a specific set. A ScrollView behaves nothing like a browser scroll container. Get any of these wrong, and the app fails silently or hangs on a white screen.
This post walks through how RapidNative — an AI mobile app builder that turns natural language into production-ready React Native and Expo apps — internally encodes UI layout constraints, guides the model to produce correct component hierarchies, and repairs the output when the model drifts. It's a look at the actual architecture, not a marketing summary.
Photo by Kevin Ku on Unsplash
Why React Native Layout Is Harder Than It Looks
Before we talk about the AI side, it helps to understand what "correct" even means here.
React Native uses Yoga as its layout engine — a C++ implementation of flexbox that ships as part of the framework. Yoga is opinionated in ways the browser is not:
- No CSS Grid. No
display: grid. If you're used to grid layouts on the web, you're rebuilding them with nested rows and columns. - No
position: fixedorsticky. Onlyabsoluteandrelative, andabsoluterequires a positioned parent. - Percentages behave differently. A
w-1/2(50% width) only resolves against a parent whose width is explicit. ScrollViewandFlatListneed bounded heights. Nesting aFlatListinside aScrollViewwith the same scroll axis throws a warning and often breaks virtualization.SafeAreaViewisn't optional. The notch, home indicator, and status bar all consume real screen space you have to explicitly reserve.
An LLM that's mostly seen web code will happily emit <div className="grid grid-cols-2"> inside a React Native file. It compiles as text. It doesn't render as a UI.
The core insight: producing valid mobile UI is a constraint-satisfaction problem, not a text-generation problem. The AI has to reason within a much smaller design space than "any HTML/CSS." Our job is to shrink the design space before generation, not clean it up afterward.
The Modular Prompt Architecture
The first place we encode layout constraints is the system prompt. But not as one giant blob — as a priority-ordered stack of sections, each independently toggleable.
Inside src/modules/api/services/ai/prompts/system-prompts.ts, the prompt is composed from twelve sections, each with an ID, a priority, an enabled flag, and content:
export const ROLE_SECTION: PromptSection = {
id: 'role',
title: 'Role & Expertise',
priority: 100,
enabled: true,
overridable: true,
content: `# Role & Expertise
You are RapidNative AI, an expert mobile development assistant
specialized in React Native and Expo...`
};
At build time, a prompt builder in config/prompt-builder.ts filters the enabled sections, sorts them by descending priority, and concatenates them. Here's what the stack looks like, roughly:
| Priority | Section | What it encodes |
|---|---|---|
| 100 | Role | The model is an expert React Native dev, not a generic coder |
| 95 | Responsibilities | Output must be complete, runnable code with no dynamic routing |
| 90 | Mobile Native | Yoga layout rules, SafeAreaView, ScrollView height constraints |
| 85 | Unsupported Tailwind | Blocklist: no grid, space-x/y, fixed, sticky |
| 84 | Import Rules | Only lucide-react-native icons, expo-router navigation, no external packages |
| 80 | Icons | Curated allowlist of 400+ verified lucide-react-native icon names |
| 75 | Tool Usage | When and how to call get_prompt_contextual_images for wireframes |
| 70 | Response Formats | XML tags: <CodeProject>, <QuickEdit>, <Action> |
| 65 | File Naming | kebab-case, no dynamic route brackets |
| 60 | UI Design | Solid colors, max 2-3 gradients, contrast ratios |
| 58 | Development Requirements | NativeWind only, max-h-* for ScrollView, no flex-1 inside auto-height |
| 55 | Critical Rules | No empty-then-fill edits, mobile-first sizing |
Why priority-ordered? Because when the context window gets tight — say, we're editing a project with a hundred files — we drop low-priority sections first. Role and Yoga constraints never get dropped. Icon allowlists and design-system pointers do. The model always sees the structural rules that make the difference between renderable and broken JSX, even when we can't fit the aesthetic guidance.
"AI mobile UI generation" gets confused with prompt-and-pray. The real work is priority-ordered constraint injection.
Photo by Amy Hirschi on Unsplash
The Component Vocabulary Rail
The second layer of constraint is what we call the vocabulary rail — a curated set of primitives and identifiers the model is allowed to use.
An unconstrained LLM will import from react-native-elements, native-base, react-native-paper, react-native-vector-icons, or invent icons that don't exist. Every one of those imports is a runtime failure. The vocabulary rail closes that door.
Three constraints do most of the work:
- Primitives only. The AI generates JSX using core
react-nativeprimitives:View,Text,ScrollView,FlatList,Image,Pressable,TouchableOpacity,TextInput,SafeAreaView,KeyboardAvoidingView. Nothing else without explicit tool use. - NativeWind for styling. All layout and styling happens through
classNameprops powered by NativeWind 4 — Tailwind CSS for React Native. This gives the model a huge, well-documented style language it already knows, minus the parts that don't apply to mobile. - A curated icon allowlist. We ship the model an inline list of ~400 verified
lucide-react-nativeicon names. When the AI wants a chevron, it picksChevronRightfrom the list — notIconChevronRight, notChevronRightIcon, notChevron_right. Every icon in the list is guaranteed to import and render.
The blocklist is just as important. From the "Unsupported Tailwind" section:
Do NOT use these Tailwind classes — they do not work in NativeWind:
- grid, grid-cols-*, grid-rows-* (use flex-row + flex-1 instead)
- space-x-*, space-y-* (use gap-* instead)
- fixed, sticky (use absolute + parent relative)
- backdrop-blur-*, filter (not supported)
- container (has no meaning in React Native)
You'd be shocked how often models emit grid grid-cols-2 for a card layout. Once you tell the model exactly what's off-limits and give it the replacement pattern, this class of error effectively disappears.
How the AI Represents Complex Hierarchies
Now the interesting part: how do we get from these constraints to correct component trees?
A mobile screen is a tree. SafeAreaView at the root. Inside that, usually a View with flex-1. Inside that, a ScrollView (or FlatList). Inside the scroll surface, sections — each section a View with its children. The tree has to be well-formed, and the layout constraints have to propagate correctly down the branches.
The system prompt gives explicit hierarchy rules. Simplified from the Mobile Native section:
- The root of every screen must be
<SafeAreaView className="flex-1 bg-*">. - Wrap scrollable content in
<ScrollView contentContainerClassName="...">— not<ScrollView className="...">for padding. - Never nest a vertical
FlatListinside a verticalScrollView. If you need mixed content above a list, split into a header +FlatList. - Use
flex-rowfor horizontal layouts,gap-*for spacing,flex-1for children that should expand. - For grids: use
flex-row flex-wrapon the parent,w-1/2orw-1/3on children.
These aren't post-processing rules. They live in the prompt at priority 90 — the model sees them before it starts generating. The result is that the tree the AI outputs is structurally sound before we ever look at the tokens.
For screens with dynamic data (a chat, a feed, a list of transactions), the model is nudged toward FlatList with a keyExtractor and a renderItem function that itself returns a well-formed subtree. For static screens, it uses ScrollView with children mapped in place.
The Two-Step Pipeline: Context Then Code
Even with a great system prompt, one-shot code generation has a failure mode: tool exhaustion. The model, given a project with existing files and access to tools like "read file" and "search," will burn its context window shuffling context around instead of writing code. By the time it reaches generation, it's out of headroom and produces truncated output.
We split generation into two calls:
Step 1 — Context Gathering (fast model)
A smaller model — typically Claude Haiku or GLM 4.6 — reads the user's prompt, inspects the project structure via tools (read_file, list_files, search_files, get_prompt_contextual_images), and produces a compressed context blob. This blob contains only what the generation step needs: relevant existing files, image URIs, the specific screen or component being modified.
Step 2 — Generation (main model) A larger model — Claude Sonnet 4.5, GLM 5, or a Bedrock-hosted equivalent — receives the compressed context plus the full system prompt and produces the code. Crucially, the generation step has no tools. It doesn't read files. It doesn't search. It writes JSX. That's it.
This split matters for hierarchies specifically. Complex screens need the model to hold multiple ideas at once: the parent screen structure, the child components' props, the state that flows between them. When we let the generation step focus entirely on writing JSX — with the file list, the design tokens, and any referenced screenshots already resolved and pasted into the context — the model can reason about the tree end-to-end without losing its place.
Model choice per step is configured in the database (ai_model_config table), so we can A/B different providers or fall back if a provider degrades. All calls go through the Vercel AI SDK — the same abstraction handles Anthropic, OpenRouter, Bedrock, Vertex, Azure, and XAI. If you want the deeper story on the pipeline, see our earlier post on why we run a 4-step LLM pipeline for React Native generation.
Photo by Markus Spiske on Unsplash
Reading Wireframes and Screenshots
Layouts don't always start as text. About a quarter of our users kick off a screen from an image — a whiteboard sketch, a Figma export, a screenshot of a competitor's app. This is where vision models come in.
When an image is attached to a prompt, RapidNative's image-to-app and whiteboard-to-app flows route it through the model slot marked VISION in ai_model_config. The vision model doesn't just describe the image — it produces a structural interpretation:
- A layout skeleton (header / body / tab bar / floating action button)
- A component list with rough hierarchy (title over subtitle over row of icons)
- A read on data patterns (this is a list, this is a form, this is a detail view)
- A best-guess on interaction (this button probably navigates, this card is probably tappable)
Two rules keep this from going wrong. First, the model is explicitly told to ignore wireframe aesthetics — grayscale, hand-drawn strokes, placeholder text — and treat them as intent, not final design. The prompt includes an "image interpretation guidelines" section that says: use a modern color palette from the brand tokens, use real copy from context, don't slavishly reproduce sketch pen strokes as borderStyle: 'dashed'.
Second, the vision step never emits code directly. It emits structured intent that the generation step turns into JSX under the same layout rules described above. This keeps the vision model's output on the same rails as the text-only path. There's only one place where JSX gets generated, and it's the place that knows the Yoga rules.
For a deeper look at this specific flow, we wrote up inside the vision prompt: how RapidNative reads a screenshot previously.
Editing a Component Without Regenerating the Screen
Understanding hierarchies isn't only about generating them from scratch. It's also about editing them without breaking them.
RapidNative's point-and-edit feature lets a user click a specific element in the preview — a button, a header, a card — and describe a change. The AI's job is to make that specific change without touching the surrounding tree.
We do this with a response format called <QuickEdit>:
<QuickEdit file="components/HeroCard.tsx">
`before`
<Pressable className="bg-blue-500 px-4 py-2 rounded-lg">
<Text className="text-white">Get started</Text>
</Pressable>
`after`
<Pressable className="bg-emerald-500 px-6 py-3 rounded-xl shadow-md">
<Text className="text-white font-semibold">Get started</Text>
</Pressable>
</QuickEdit>
The AI emits an exact before block (the code as it currently exists) and an after block (the code with the change applied). Our message transformer (src/shared/utils/messageTransformer.ts) parses these tags out of the stream, verifies the before block exists verbatim in the target file, and applies the replacement.
Two design choices matter here. First, the AI never regenerates the whole file when it only needs to change a button color. That preserves component identity — the parent hierarchy is untouched, the sibling components are untouched, state and refs stay intact. Second, the before/after format is trivially reversible — undo is just applying the diff in the other direction.
For this to work, the AI needs to know which subtree it's editing. When a user clicks an element, the editor sends the selected component's JSX snippet, its file path, and its line range along with the prompt. The AI has to understand not just the visual element but the position of that element in the component tree. This is where the Redux state layer comes in.
The Editor's Model of the Tree
The editor's Redux store — specifically src/modules/editor/store/slices/editorSlice.ts — tracks a small but critical set of fields:
files: a map of file path → contents. The virtual file system.artboards: a map of artboard ID → screen data (JSX, x/y position, device size).selectedLayer: the currently selected component node.selectedLayerDimensions: its bounding box in the preview.selectedArtboard: the screen that layer belongs to.selectionMode: whether we're in point-and-edit mode.deviceDimensions: iPhone 15 Pro, iPad Air, etc. — used both for preview and to tell the AI the target viewport.
When the user makes an edit, the payload sent to the generation route includes filePath, specificCodeContext (the JSX subtree the user selected), the artboard ID, and the current deviceDimensions. The AI never has to guess "which button did they mean" — the selection state resolves that ambiguity before the LLM ever sees the prompt.
We covered this in more depth in visual editor for React Native: how point-and-edit works under the hood.
Post-Processing: The Safety Net
Even with all these constraints, models occasionally produce imperfect output — an unclosed View, a missing import, a TypeScript generic that lexes like JSX. Post-processing is the safety net.
src/shared/utils/jsxFixer.ts runs a custom AST walker over the model's output. It closes unclosed tags, distinguishes TypeScript generics like <T,> from JSX tags, and repairs incomplete function returns. It's tolerant enough to fix partial streams — important because we render output as it streams, not after the whole response has landed.
src/shared/utils/postProcessor.ts runs afterward. It auto-imports React, common React Native primitives, and any lucide-react-native icons the code references. It merges package.json additively — if the AI wants a new dependency, we add it, but we never remove an existing one. And it runs Prettier so the formatted output looks like the rest of the codebase.
None of this is "AI cleaning up after itself." It's deterministic code with well-defined rules, running after generation, catching the small class of errors we can catch with static analysis instead of trusting the model.
Photo by William Hook on Unsplash
A Concrete Example: The Fitness Dashboard
Let's ground this. A user opens RapidNative and types: "Build me a fitness dashboard with a header showing today's stats, a scrollable list of workouts, and a floating action button to start a new workout."
Here's what happens under the hood, in order:
- Context gathering. Haiku reads the prompt, sees there's no existing project, and returns a compact context: "New project. No files yet. Prompt is a single-screen fitness dashboard with 3 sub-components."
- Generation call setup. The system prompt loads with all sections at priority ≥55. The role, Yoga rules, Tailwind blocklist, icon allowlist, and response format all get pasted in.
- Model output starts streaming. Claude Sonnet 4.5 emits
<CodeProject>and inside it a file block forapp/index.tsx. The tree it produces:
<SafeAreaView className="flex-1 bg-slate-50">
<View className="px-4 pt-4 pb-2">
<Text className="text-2xl font-bold">Today</Text>
<View className="flex-row gap-3 mt-3">
<View className="flex-1 bg-white rounded-2xl p-4">
<Text className="text-xs text-slate-500">Steps</Text>
<Text className="text-xl font-semibold">8,247</Text>
</View>
{/* two more stat cards */}
</View>
</View>
<ScrollView className="flex-1" contentContainerClassName="px-4 pb-24">
<Text className="text-lg font-semibold mt-4 mb-2">Recent workouts</Text>
{/* mapped workout rows */}
</ScrollView>
<Pressable className="absolute bottom-6 right-6 bg-emerald-500 w-14 h-14 rounded-full items-center justify-center shadow-lg">
<Plus color="white" size={28} />
</Pressable>
</SafeAreaView>
- Post-processing. JSX fixer confirms the tree is well-formed. Post-processor adds
import { SafeAreaView, View, Text, ScrollView, Pressable } from 'react-native'andimport { Plus } from 'lucide-react-native'. Prettier runs. - Preview mounts. The Expo browser bundler compiles the file and mounts it in the preview iframe.
The hierarchy is correct on the first pass. The stat cards use flex-row gap-3 instead of a broken grid. The FAB uses absolute on top of a flex-1 parent — legal. The ScrollView reserves 96px of bottom padding so the FAB doesn't clip the last row. The icon comes from the allowlist.
None of this happened because the model is magical. It happened because the design space was small enough that the model's first plausible completion is also a correct one.
Why This Matters
The industry framing of AI mobile UI generation tends to be "prompt → screen" as if the model is doing all the work. In practice, the model is doing one part of the work — token-by-token synthesis — inside a much larger system that defines what "valid" means, feeds the model the right context, catches its mistakes, and gives users a way to make surgical edits after the fact.
For teams building on RapidNative, this shows up as a simple property: the first render usually works. Not because we're hoping for the best, but because Yoga rules are baked into priority 90 of the system prompt, the icon allowlist prevents import errors, the two-step pipeline gives the generation model room to reason, and the post-processor catches the last mile.
If you're evaluating AI app builders — RapidNative or otherwise — this is the layer worth digging into. Ask what happens when the model gets it wrong. Ask what constraints live in the system prompt versus what gets fixed after. Ask how the tool handles nested FlatLists or SafeAreaView on notched devices. The tools that treat this as an afterthought will produce plausible code that doesn't render. The tools that treat it as an architecture problem will produce React Native apps that actually work.
Try it for yourself at rapidnative.com — describe a screen, and watch the tree come out well-formed on the first pass.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.