Make it yours
Give the app your name, bundle id, colours (light and dark), icon, splash, font and onboarding copy - and check each one landed.
Hey - this is the first thing I'd do with a fresh tree. By the end the app shows your name, your
colours in light and dark mode, your icon and splash, your font and your onboarding copy, and
bun run doctor stops warning about placeholders.
Before you start
- Tier: any. The Free tier has no
gen:*scripts and nodoctor, so there you edit the generated files by hand (each step says which). - Time: about 30 minutes, plus a native rebuild at the end.
- Runs in: Expo Go for colours, font and onboarding. The name, icon and splash are native, so you only see them in a dev build or a store build - Expo Go always shows its own. See Expo Go or a dev build.
- Accounts: none.
- Previous tutorial: Quickstart - the app runs and
setupis done.
Almost everything lives in one file, readynative.config.ts at the repo root. The full field
list is in Config; this page covers the branding fields in the order I'd do
them.
1. Set the name and ids
Open readynative.config.ts and change the app block. Here's the whole file with example
values - keep your own urls, store, links, features and privacy if you already set
them. On the Free tier the file has no features line; don't add one.
import type { ReadyNativeConfig } from "./src/lib/readynative-config";
export default {
app: {
name: "Trailmix",
slug: "trailmix",
scheme: "trailmix",
owner: "",
easProjectId: "",
ios: { bundleId: "com.yourcompany.trailmix" },
android: { package: "com.yourcompany.trailmix" },
},
brand: { primary: "#0f766e", accent: "#f59e0b", radius: 12, font: "Inter" },
urls: { website: "", support: "mailto:", privacy: "", terms: "" },
store: { appStoreId: "", playPackage: "" },
links: { domain: "", appleTeamId: "", androidCertFingerprints: [] },
features: { reviewPrompt: true, updatesBanner: true, tracking: false },
privacy: { askEverywhere: false },
} satisfies ReadyNativeConfig;name is the label under the icon, scheme is the deep-link prefix (trailmix://), and the
two ids are what the App Store and Google Play know your app by. Pick the ids before the first
store build: you can't change them on a published app. Dev and preview builds append .dev /
.preview to both ids and (Dev) / (Preview) to the name, so all three install side by
side.
bun run doctorYou should see: no more warnings for app.name or the com.acme.app ids. The Home tab's
title already reads your name; the label under the icon changes with the next native build
(step 5).
2. Pick your brand colours
brand in the same file drives the palette: primary becomes the light-mode primary token,
accent the brandAccent token, and radius the base corner radius (radius.lg; the other
sizes derive from it). They flow through src/theme/tokens.ts. Regenerate the files your UI
stack reads:
bun run gen:themeThat rewrites src/global.css + tailwind.config.js (NativeWind 4), src/global.css
(NativeWind 5) or tamagui.config.ts (Tamagui); StyleSheet and Unistyles read tokens.ts
directly. On the Free tier, which has no gen:theme, also change the --primary HSL triple (for
example --primary: 221 83% 53%;) in the :root block of src/global.css by hand. Then
restart with a clean cache so Metro picks up the new theme:
bun run start -cYou should see: primary buttons and the active tab icon in your primary colour.
3. Set the dark-mode colours
In dark mode primary is a neutral light grey, not your brand colour - a dark brand colour on a
near-black background is hard to read. To keep your brand in dark mode, pick a lighter shade of
it and set it in the dark palette of src/theme/tokens.ts:
dark: {
// ...the other tokens stay as they are
primary: "#5eead4",
primaryForeground: "#042f2e",
// ...
},primaryForeground is the text on primary buttons, so keep the pair readable. Then regenerate:
bun run gen:themeYou should see: in Settings → Appearance → Dark, primary buttons and the active tab use your dark shade.
4. Load your font
brand.font is only a family name, exported as fontFamily from src/theme/tokens.ts.
Nothing loads the font file for you, so until you do, the app renders the system font.
Put the file in assets/fonts/, then load it in src/app/_layout.tsx and keep the splash up
until it's ready. The key you pass to useFonts must equal brand.font. Here's the template's
root layout with the font added:
import { useFonts } from "expo-font";
import { Stack, usePathname } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { AnimatedSplashOverlay } from "@/components/animated-icon";
import { UpdatesBanner } from "@/components/updates-banner";
import { useAuthRedirect } from "@/hooks/use-auth-redirect";
import { useOnboardingRedirect } from "@/hooks/use-onboarding-redirect";
import { useSignOutCleanup } from "@/hooks/use-sign-out-cleanup";
import { analytics } from "@/lib/analytics";
import { Providers } from "@/providers";
import { hydrateThemeMode } from "@/stores/theme-mode";
export { ErrorBoundary } from "@/components/error-boundary";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [fontsLoaded, fontError] = useFonts({
Inter: require("../../assets/fonts/Inter-Regular.ttf"),
});
const ready = fontsLoaded || fontError !== null;
useEffect(() => {
if (!ready) return;
// Single hydration point: the theme provider only reads the store.
hydrateThemeMode().finally(() => {
SplashScreen.hideAsync().catch((err: unknown) => {
console.warn("[splash] hideAsync failed", err);
});
});
}, [ready]);
if (!ready) return null;
return (
<Providers>
<AnimatedSplashOverlay />
<UpdatesBanner />
<RootStack />
</Providers>
);
}
/** Inside `Providers` so redirect hooks can read stores/providers; routes come from `src/app`. */
function RootStack() {
useOnboardingRedirect();
useAuthRedirect();
useSignOutCleanup();
const pathname = usePathname();
useEffect(() => {
analytics.screen(pathname);
}, [pathname]);
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="examples" />
<Stack.Screen name="+not-found" />
{/* gen:stack */}
</Stack>
);
}Your root layout may differ slightly - the Free tier has no UpdatesBanner and no
{/* gen:stack */} marker, and screens you generated with gen screen --modal add
<Stack.Screen> lines. Keep those; only the useFonts lines, the ready guard and the
[ready] dependency are new. expo-font is already a dependency, and useFonts works in Expo
Go.
Your kit's Text doesn't read fontFamily, so pass it in src/components/ui/text.tsx.
Add fontFamily to the tokens import, and put it first in the style array so a style prop
still wins:
import { fontFamily, fontWeight } from "@/theme/tokens";
// ...inside Text, the style prop of <RnrText>:
style={[
{ fontFamily },
color ? { color: colors[color] } : null,
weight ? { fontWeight: fontWeight[weight] } : null,
align ? { textAlign: align } : null,
style,
]}Bold weights need their own file and their own family name, for example "Inter-SemiBold".
You should see: every screen in your font after a reload. If text still looks like the
system font, the useFonts key doesn't match brand.font.
5. Replace the icon and splash
Drop a square PNG, at least 1024 px, at assets/brand/icon.png, then:
bun run gen:assetsIt writes assets/images/icon.png, the three Android adaptive icon layers
(android-icon-foreground/background/monochrome.png), splash-icon.png and favicon.png, on a
brand.primary background. Pass --source path/to/icon.png to read from another file. It runs
under Node because sharp hangs inside bun. On the Free tier, replace those files in
assets/images/ by hand.
The splash is the expo-splash-screen plugin in app.config.ts: splash-icon.png, 76 pt wide,
on brand.primary. AnimatedSplashOverlay (src/components/animated-icon.tsx) shows the same
image for the first frame and fades it out, so both stay in sync.
Optional: an Icon Composer icon for iOS
iOS uses assets/images/icon.png like every other platform. To ship a layered Icon
Composer icon instead, export a .icon file to
assets/expo.icon: app.config.ts sets ios.icon to it only when that file exists, and
gen:assets never touches it.
The name, ids, icon and splash are native, so rebuild to see them. bun run ios compiles the
dev build locally (it needs Xcode; bun run android needs Android Studio), or you can build one
on EAS - see Expo Go or a dev build.
bun run iosYou should see: your icon and "Trailmix (Dev)" on the home screen, and your splash on launch.
6. Rewrite the onboarding
The three slides are the SLIDES array at the top of
src/screens/onboarding/onboarding-screen.tsx. Each slide has an SF Symbol (icon), a Material
fallback for Android and web (fallback), and two strings; {{name}} becomes your app name.
const SLIDES: readonly Slide[] = [
{
icon: "map",
fallback: "map",
title: "Welcome to {{name}}",
description: "Log every trail you walk, with the photos and notes that go with it.",
},
{
icon: "camera",
fallback: "photo_camera",
title: "Snap as you go",
description: "Photos land on the trail they were taken on - no sorting later.",
},
{
icon: "checkmark.seal",
fallback: "verified",
title: "You're all set",
description: "Start your first trail from the Home tab.",
},
];The strings are i18n keys (the English text is the key). With an i18n module selected, add each
new string to every src/locales/*.json - a test checks all locales have the same keys - and
delete the old slide keys.
You should see: in Settings → Reset onboarding, then a relaunch, your three slides.
Check it
bun run doctorIt warns while app.name is still ReadyNative, the ids are still com.acme.app, or the
urls and store fields are empty. Fill those in before your first store build - the Settings
screen links to urls.privacy, urls.terms and urls.support. The Free tier has no doctor,
so read those fields in readynative.config.ts yourself. Then:
bun run typecheckIf it doesn't work
- Colours didn't change - run
bun run gen:themeagain (on Free, check the--primarytriple you edited insrc/global.css), then restart withbun run start -- -c. NativeWind caches the old CSS. - The icon or name is still ReadyNative's - you're in Expo Go, or in a dev build made before
the change. Rebuild with
bun run ios/bun run androidor a new EAS build. gen:assetshangs or fails - it needs Node >= 22.18 and a square PNG of at least 1024 px atassets/brand/icon.png. On Free there is nogen:assets: the files you replaced inassets/images/must keep their names and be square PNGs.- "Unable to resolve ../../assets/fonts/…" - the path in
require()is relative tosrc/app/_layout.tsx, and the file name must match exactly, case included. - A locale test fails after editing the slides - with an i18n module and jest, a new string
is missing from one of the
src/locales/*.jsonfiles. The Free tier has neither.
More in Troubleshooting.
Congrats 🎉
It's your app now: your name, ids, colours in both modes, icon, splash, font and first-run copy. Next, build something in it: Add your first feature.
Expo Go or a dev build
Which modules run in Expo Go, which need a development build, and how to make one - locally with Xcode or Android Studio, or in the cloud with EAS.
Add your first feature
Build a Notes tab end to end - a route, a data hook against a public API, a persisted drafts store, a validated form, a list and a test.