How RapidNative Generates Theme-Aware React Native UIs
By Suraj Ahmed
29th Jun 2026
Last updated: 29th Jun 2026
Most "dark mode in React Native" guides teach you the same five things: call useColorScheme(), wire up a Context provider, swap colors based on the scheme, persist the user's choice, and don't forget the status bar. They're correct, and they're also where the real problem starts.
Because when an AI generates the code, none of that is automatic. The model has to understand that an app should respond to light and dark mode in the first place — and then it has to write code that does so consistently across every screen, every component, every inline style prop, and every native primitive that doesn't accept className. One slip and you get a screen that looks fine in dark mode and unreadable in light mode (or vice versa). Multiply that across a 12-screen generated app and the cracks show up everywhere.
This post walks through the actual approach we built into RapidNative — an AI mobile app builder that turns plain English into React Native code — to make theme-aware UI the default, not the exception. We'll cover the three-layer architecture, the prompt rules we hand the LLM, the design decisions that let users skip writing color-scheme logic entirely, and what the exported code actually looks like when you download a project.
Theme-aware UI means the same screen renders correctly in both modes without conditional logic in every component — Photo by UX Indonesia on Unsplash
What does theme-aware mean in a React Native app?
A theme-aware React Native UI is one where every visible element — backgrounds, text, borders, icons, status bar tint, even native modal overlays — adapts to the user's selected color scheme without per-screen conditionals. In practice this means a single set of semantic color tokens (like background, foreground, card, border) that resolve to different RGB values in light versus dark mode, with the switch handled at the root of the app instead of inside every component. It's the difference between writing <View className="bg-background"> once and writing <View style={{ backgroundColor: isDark ? '#191919' : '#ffffff' }}> in 40 places.
The problem with "just use useColorScheme"
The standard answer for dark mode in React Native is the useColorScheme hook — read the OS color scheme, branch on it, apply the right color. It works for hand-written code. It falls apart for AI-generated code for three reasons:
- The model has to remember to do it every time. Every new component, every new screen, every refactor. Miss one and you get a half-themed app.
- It encourages conditional clutter.
isDark ? colors.darkBg : colors.lightBgeverywhere — twice the surface area, twice the bug count. - It doesn't help with native primitives. Components like
ActivityIndicator,RefreshControl, andSwitchtake acolorprop, not a className. They need hex values regardless of how the rest of the app is styled.
So we made a different choice: take dark mode away from the AI as a per-component decision and move it to the foundation of the generated project. The model never branches on color scheme. It writes semantic class names. The theme system handles the rest.
Generated code stays clean when the theme system handles the light/dark switching instead of every component — Photo by Lukas Blazek on Unsplash
The three-layer architecture
RapidNative has a theming system at three distinct layers, and it helps to keep them separate before getting into the code:
| Layer | Where it lives | What it themes |
|---|---|---|
| Marketing site | next-themes + Tailwind CSS variables | The rapidnative.com public site (light by default) |
| Studio (editor) | next-themes + studio-specific CSS variables | The authenticated app where you build projects (dark by default) |
| Generated app | NativeWind v4 vars() + a scaffolded theme.ts | The React Native app the AI produces for you |
The first two are standard Next.js patterns. The third is where the interesting work happens — that's the layer the AI actually generates code into. We'll cover all three, but most of this post is about layer three.
Layer 1: the marketing site
The website uses next-themes wired up in src/shared/components/RootProviders.tsx. The provider is configured with attribute="class" so the active theme is reflected as a class on <html>, defaultTheme="dark" (a deliberate choice for the brand), enableSystem={false} to ignore the OS scheme on landing, and a custom storageKey="rapidnative-theme".
The marketing routes under src/app/(site)/ then override that default back to light via a sync component (src/app/(site)/SiteThemeSync.tsx) that listens to resolvedTheme from useTheme() and toggles the class on document.documentElement. The result: the public site reads as a clean light surface, and the editor reads as the dark working environment most developers prefer.
Layer 2: the studio editor
The studio layout (src/app/(studio)/layout.tsx) loads a separate stylesheet — src/app/studio-globals.css — that defines the editor's own light and dark palettes in OKLCH color space, the perceptually uniform color space we adopted to keep contrast consistent across hues. A second sync component (src/app/(studio)/StudioDarkMode.tsx) handles the studio's default-to-dark behavior and — critically — injects a tiny inline script that runs before React hydrates. That script reads the persisted theme from localStorage and applies the class to <html> immediately, eliminating the flash of unstyled content that otherwise appears on first paint.
This is a small detail with a big payoff: nothing kills the perception of a polished product faster than a white flash on every page load in dark mode.
Layer 3: the generated app
This is the one users actually take with them. When the AI generates a project, every new app starts from a scaffold that includes:
theme.ts— a single file with two exports,lightThemeanddarkTheme, each declared via NativeWind v4'svars()helpersrc/providers/ThemeProvider.tsx— a scaffolded provider that wires the active theme to the root view (the AI is forbidden from editing this)tailwind.config.js— NativeWind v4 configuration that maps semantic class names to the CSS variables intheme.ts
When a screen needs a background color, the AI writes className="bg-background". NativeWind resolves bg-background to var(--background) at runtime. --background is one set of RGB values when lightTheme is active and another when darkTheme is active. The screen renders correctly in both modes — no conditionals, no useColorScheme, no per-component branching.
How we instruct the AI to generate theme-aware code
Most posts on AI code generation stop at "the model writes the code." That's underselling what actually happens. A working system prompt for theme-aware generation has to do four things at once: explain the system, forbid the wrong patterns, define the surface area the AI can touch, and provide enforcement examples.
Here's the shape of the rules we encode in our system prompt (lives in tools/project-templates/fullstack/ai/prompts/system-prompt.ts):
Semantic-first styling
"Use semantic classes — they auto-adapt to light/dark. No
dark:prefix, no arbitrary bracket values inclassName, no hardcoded hex inclassName."
The model is explicitly told that semantic tokens (bg-background, text-foreground, bg-card, text-muted-foreground, border-border) are the only acceptable way to express color in className. This single rule eliminates the entire class of bugs where one screen uses bg-white (which never adapts) and another uses bg-background (which does).
Theme.ts as a fill-in-the-blanks file
"
theme.tsis a fill-in-the-blanks file — only replace the RGB values. Keep every import, every export name (lightThemeanddarkTheme), every CSS variable key character-for-character identical to the scaffold."
The model can change colors. It cannot restructure the file. This invariant is what lets the rest of the system — the parser in the editor, the export pipeline, the theme editor UI — assume a predictable shape.
ThemeProvider is off-limits
"
src/providers/ThemeProvider.tsx— novalueprop. Calling<ThemeProvider value={...}>is invalid and breaks dark mode."
LLMs love to pass props that look reasonable. A value prop on a context provider looks reasonable. It would also silently destroy theme switching. So we forbid it by name, with the consequence spelled out.
Inline color rule for native primitives
"When a
styleprop requires a hex value (tab bar, RefreshControltintColor, ActivityIndicator, Switch, Modal overlay), derive the hex from YOURtheme.tsRGB values and use theisDarkternary."
This is the one case where the model does need to branch — RefreshControl and friends don't accept className, so a hex color computed from the active theme is the only option. We allow it, but only when it's the only option, and only with values that come from theme.ts (so they stay in sync if the user customizes the palette later).
No unrequested variables
"Do not add new variables (
useColorScheme,SplashScreen.hideAsync, etc.) that the user didn't ask for."
The model is told, in those words, not to inject useColorScheme calls. This keeps generated screens free of the boilerplate that competing tools tend to scatter everywhere.
The result of these five rules together is that across hundreds of generated screens, the color story stays identical: semantic classes everywhere, theme switching at the root, and the rare inline color always grounded in the user's actual palette.
The same screen in light and dark mode — possible because color decisions live in one place, not 40 — Photo by Daniel Romero on Unsplash
Brand-tinted palettes: deterministic dark mode color generation
There's a subtler problem most app builders skip entirely: every generated app shouldn't look the same. If you ask for a fitness app, the brand should lean energetic. If you ask for a meditation app, it should feel calm. And the dark mode palette should hold the same character as the light one — same brand hue, same accent — not just inverted grays.
We handle this in a module called the GLM design directive (tools/project-templates/fullstack/ai/prompts/glm-design-directive.ts). When a project is created, we derive a brand hue and accent hue from a hash of the project ID — deterministic, so the palette stays stable across regenerations, but unique per project. From those two hues, we generate matched light and dark palettes via HSL-to-RGB conversion, and we hand the model a pre-composed theme.ts it's told to emit verbatim on the first generation.
The directive also assigns the project one of eight predetermined visual styles — editorial, bento grid, full-bleed media, minimalist list, data-dense, soft rounded consumer, brutalist, or glass/layered depth — and the system prompt enforces consistency with that style on every new screen. So a project that started in "soft rounded consumer" stays soft and rounded as it grows; one that started in "brutalist" stays brutalist.
Why this matters for dark mode specifically: lots of dark themes look fine in isolation but feel disjointed when paired with their light counterparts. Generating both from the same hue, in the same color space, in the same call, keeps the two modes feeling like one product instead of two near-strangers.
The theme editor: customize colors after generation
Generated palettes are a starting point, not a contract. Users frequently want to tweak the brand color, soften a border, or push the dark background a shade warmer. The Theme Editor (src/modules/studio/components/custom/editor/ThemeEditor/ThemeEditorPanel.tsx) is the UI for that.
Under the hood, the editor:
- Parses
theme.tswith a regex-based parser (themeParser.ts) that pulls out thelightThemeanddarkThemeblocks, the radius value, and the individual color entries. - Renders two grids — one for light mode colors, one for dark mode — with OKLCH/RGB color pickers per token.
- Tracks state in a dedicated Redux slice (
src/modules/editor/store/slices/themeEditorSlice.ts) with actions likeupdateColor,updateRadius,addColor, anddeleteColor. - Writes back to
theme.tson save, which triggers the project's virtual file system to rebuild and the live preview to reload — typically in under a second.
Because the parser depends on the file's exact scaffolded shape, the "don't restructure theme.ts" rule we feed the AI isn't just defensive — it's what makes interactive editing possible at all. If the model ever rewrote the file, the editor would lose its handle on it.
The live preview: seeing both modes immediately
A theme system that only works in production is useless during iteration. The in-editor preview (src/modules/studio/components/custom/editor/ProjectPreview.tsx) renders generated apps in an iframe using our custom in-browser React Native runtime, with a toggle that flips between light and dark instantly. Every change to theme.ts propagates through the file watcher to the iframe in real time — change a color, see it everywhere, in both modes, in seconds.
This loop is part of why the prompt rules matter so much. If a screen used bg-white instead of bg-background, the preview would expose it immediately when you flipped to dark mode. Because every AI-generated screen uses semantic tokens, that exposure rarely happens — and when it does, it's a regression worth investigating, not the daily reality.
What you actually get when you export
When you download a project from RapidNative, the export pipeline (src/app/api/user/projects/[projectId]/download/route.ts) bundles a complete React Native + Expo workspace including:
theme.tswith your current light and dark colorssrc/providers/ThemeProvider.tsx— the scaffolded provider, unchangedtailwind.config.js— NativeWind v4 config that binds class names to your CSS variables- All app screens using semantic classes that resolve correctly in both modes
package.jsonwithnativewind,tailwindcss,expo, and the supporting dependencies pinned
The exported app is yours. No runtime dependency on RapidNative, no proprietary theming layer, no lock-in. It's a standard React Native + Expo project that happens to have well-organized theming because the AI was given strict rules about how to build it.
This connects to a broader principle: the value of an AI app builder isn't the chat interface. It's the structural quality of the code it produces. We wrote about this from a different angle in our take on why AI-generated code beats React Native templates, and the theme system is one of the clearest examples of it in practice.
How this compares to the standard approaches
For developers who've implemented dark mode by hand, here's where our approach sits relative to the common patterns:
| Approach | What it solves | What it leaves to you |
|---|---|---|
useColorScheme + conditional colors | OS scheme detection | Persistence, every component's branching, native-primitive colors |
| React Navigation themes | Navigator background and header colors | Screen content colors, custom components |
| Restyle | Typed theme tokens, variants | Wiring up every component, manual theme switching |
| NativeWind v4 vars() | Class-based theming with CSS variables | Defining the palette, structuring the file, instructing your team to use it |
| RapidNative's approach | All of the above, generated and enforced by the AI | Nothing — the scaffold ships ready, and the rules keep new code consistent |
The last row is the only one where the theme system is created for you and policed by the generator itself. That's the entire point: dark mode stops being a project you start and becomes a property of every project you generate.
What this unlocks
Three concrete things become easier when theme-aware UI is the default:
- Cross-team handoff. A founder generates a fitness app and sends the export to a contractor. The contractor doesn't have to add a dark mode story — it's already there, on every screen, ready to ship.
- Brand changes without rewrites. Swap the primary hue in
theme.tsand the whole app reflows to the new brand. Both light and dark. - Lower regression risk. With color decisions in one file, you don't have a 12-screen audit every time you change a brand color. You have one diff.
We've found that the projects users invest in long-term are almost always the ones with clean theming, because that's the proxy for "this is structured enough that I trust extending it." A messy theme story is usually a sign that the rest of the code will be messy too.
Build a theme-aware app from a prompt
If you want to see this in action, start a project on RapidNative and ask for something like "a habit tracker with a clean minimalist aesthetic, soft purples, and a dark mode that feels calming." You'll get back a React Native + Expo project with both palettes wired up, every screen rendering correctly in both modes, and a theme.ts you can tweak in the in-editor theme editor or by hand after exporting.
You can also explore other slices of the same generation pipeline — like how we generate NativeWind styles from natural language, how the export pipeline ships your app to the App Store, or how the visual point-and-edit system works.
The theme system is one piece of a larger philosophy: the best part of an AI app builder isn't the moment of generation. It's everything the generated code lets you do afterwards. Theme-aware UI is one of the clearest examples — the AI does the boring part once, correctly, and you spend your time on the part that's actually yours.
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.