Customize your app's colors and layout in config/ruby_native.yml.
appearance:
tint_color: "#007AFF"
background_color: "#FFFFFF"
The tint_color controls the active tab icon color and any tinted UI elements.
The background_color is used as the window background, visible during app launch and view transitions. Set it to match your CSS body background to avoid a flash of white before content loads. If omitted, the default is white.
By default, the app follows the device's system appearance. Set theme to force light or dark mode regardless of the user's device setting.
appearance:
theme: light
| Value | Behavior |
|---|---|
auto |
Follows the device setting (default) |
light |
Always light mode |
dark |
Always dark mode |
When omitted, theme defaults to auto.
Color fields accept a plain hex string or an object with light and dark keys.
appearance:
tint_color: "#007AFF"
background_color:
light: "#FFFFFF"
dark: "#212529"
The app picks the matching value based on the current appearance. This follows the device setting by default, or the forced theme if you set one. If you pass a plain string instead of an object, that color is used for both modes.
Brand the navigation bar across your whole app: a centered logo in place of the page title, your own bar colors, and a matching status bar.
appearance:
navbar:
logo: "<%= image_url('logo.png') %>"
background_color: "#3B3F54"
foreground_color: "#FFFFFF"
status_bar: light
Everything in the navbar block is optional. Set only the colors for a themed bar with text titles, or add a logo to replace those titles.
Set logo with image_url. config/ruby_native.yml is evaluated as ERB, and the fingerprinted URL means the app downloads the logo once, caches it, and re-downloads it only when the image changes.
logo: "<%= image_url('logo.png') %>"
A full URL works too, for a logo hosted on a CDN.
Use a PNG (or any raster format) with a transparent background; SVGs are not supported. The logo renders about 30 pt tall with its width following the aspect ratio, so export it at roughly 3x that (around 90 to 120 px tall) and keep it a compact wordmark so it doesn't crowd the back button.
The logo replaces the page title and stays centered on every screen, including during the native push and pop transitions in Advanced Mode. Leading and trailing buttons added with native_navbar_tag still appear as usual.
background_color sets the bar's background, and foreground_color sets the title and bar button color. Each accepts a hex string or a { light:, dark: } object, like the dark mode colors above.
Set status_bar to keep the clock and system icons readable against your bar: light for white content on a dark bar, dark for dark content on a light bar. Omit it to let the system decide; it is not derived from your bar color.
On Android, the navigation bar, tab bar, and floating action button are a neutral gray, light or dark to match the theme and sitting one step off the page background, with your tint_color on the selected tab and accents.
Customize your navigation bar or tab bar under the android key:
appearance:
android:
navbar:
background_color: "#1E293B"
foreground_color: "#FFFFFF"
tab_bar:
background_color: "#1E293B"
foreground_color: "#FFFFFF"
Each accepts a hex string or a { light:, dark: } object, like the dark mode colors above. On Android these win over the cross-platform navbar branding colors.
The navbar's foreground_color sets the title and icon color; when omitted it's black or white, picked for contrast against the bar.
The tab bar's foreground_color sets every tab icon and label. The selected tab uses it at full strength and the rest dim to 60% of it, so one color covers both states. When omitted, the selected tab takes your tint_color and the rest follow the Material theme, which is hard to read on a dark branded bar in light mode.
These settings never touch iOS. Its bars are Liquid Glass, which Apple doesn't let apps recolor, so they always keep the system look.
On iOS, show the launch screen with an activity indicator while the app loads, instead of flashing to a blank web view. Set enabled: true to turn it on.
Android needs no additional configuration. The system splash screen and your icon on your background color always remain visible until the first page renders.
appearance:
splash:
enabled: true
spinner_color: "#007AFF"
status_bar: light
Everything except enabled is optional:
spinner_color sets the color of the activity indicator. By default it picks black or white from your background_color so it stays visible; override it with your own color, such as a brand accent. Accepts a hex string or a { light:, dark: } object, like the dark mode colors above.status_bar sets the status bar content over the splash: light for white content on a dark background, dark for dark content on a light one. By default it is chosen to stay readable against your background_color.The splash shows on every launch except the very first after install, when your config hasn't been cached yet.
Android doesn't read this block. It always shows the system splash screen, holding your launch icon on your background_color until the first screen paints, so there's nothing to enable and no spinner to color.
The web view always extends behind the status bar and Dynamic Island. Your web page controls the entire screen, including the area behind system UI.
Add viewport-fit=cover to your viewport meta tag so CSS env(safe-area-inset-*) variables return real values.
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
The gem stylesheet provides utility classes to add safe area spacing. These stack with your existing padding and margin utilities.
<%= stylesheet_link_tag :ruby_native %>
| Class | Effect |
|---|---|
native-inset |
Adds safe area spacing at top and bottom |
native-inset-top |
Adds safe area spacing at top only |
native-inset-bottom |
Adds safe area spacing at bottom only |
<main class="px-4 pb-8 native-inset">
<%= yield %>
</main>
These classes use ::before and ::after pseudo-elements, so they stack with padding utilities like pb-8 without conflicting. Background colors extend through the inset area.
For position: fixed navbars, use native-inset-top directly on the navbar element. The navbar background extends behind the status bar while its content is pushed below the Dynamic Island.
<nav class="fixed top-0 left-0 right-0 bg-white native-inset-top">
<!-- nav content -->
</nav>
The content area below a fixed navbar still needs its own top padding to clear the navbar, plus native-inset-top for the safe area.
Navbars pinned to top: 0 can use native-inset-top because the spacer pushes their content down. Overlays anchored at an offset can't: a banner at top: 0.75rem or a toast stack near the top of the screen needs the offset itself to account for the safe area. Anchor these with max() and the gem's safe area variables:
.banner {
position: fixed;
top: max(0.75rem, calc(var(--ruby-native-safe-area-top) + 0.5rem));
}
Or inline with Tailwind:
<div class="fixed inset-x-3 top-[max(0.75rem,calc(var(--ruby-native-safe-area-top)+0.5rem))]">
The same applies to full-screen modals: give the modal container padding-top: max(0.75rem, var(--ruby-native-safe-area-top)) so its header and close button sit below the status bar.
--ruby-native-safe-area-top and --ruby-native-safe-area-bottom come from the gem stylesheet and resolve everywhere. On iOS they read env(safe-area-inset-top), on Android the app pushes in the real system bar heights, and on the web they fall back to 0px, so they're safe in CSS that also serves your website.
Don't use raw env(safe-area-inset-*) for these rules. Android's WebView doesn't populate it from system bars, so an overlay that looks right on iOS silently sits under the Android status bar.
Web defaults give an app away: a gray flash on every tap, text selection on a long-pressed button, and a zoom on an accidental double tap. The gem stylesheet turns those off inside the app, and only there, so your website keeps its normal behavior:
:active styles apply instantly, so pressed states feel immediate.There's nothing to configure. The rules apply automatically whenever your layout includes the gem stylesheet:
<%= stylesheet_link_tag :ruby_native %>
For pressed feedback that matches native, style :active on your buttons and rows, for example a slightly darker background or reduced opacity.
When a page rubber-bands past its edges, the area revealed behind it is the window background_color. That breaks down when a page has, say, a dark header and a light footer: one color can't match both edges. native_overscroll_tag sets the colors revealed beyond the top and bottom of a specific page.
<%= native_overscroll_tag top: "#1E293B", bottom: "#FFFFFF" %>
import { NativeOverscroll } from "@ruby-native/react"
<NativeOverscroll top="#1E293B" bottom="#FFFFFF" />
<script setup>
import { NativeOverscroll } from "@ruby-native/vue"
</script>
<template>
<NativeOverscroll top="#1E293B" bottom="#FFFFFF" />
</template>
bottom: is optional and falls back to top:, so one color covers both edges. Like the other signal helpers it applies per page, so each screen can match its own design. Works on both platforms, in both modes.
By default, apps run in portrait only on phones. Set landscape: true to let phones rotate into landscape left and landscape right as well.
appearance:
landscape: true
| Value | Behavior on phones |
|---|---|
false or omitted |
Portrait only (default) |
true |
Portrait, landscape left, and landscape right |
Tablets always rotate freely, regardless of this setting. iPads and newer Android tablets ignore orientation locks at the system level, so locking would only apply to older Android tablets and split behavior by OS version.
Make sure your layouts work at landscape widths before enabling this, and at tablet widths if you support tablets. Test fixed-width elements, full-height sections, and any CSS that assumes a narrow viewport.