●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR scanning, Metal 3D, widgets, Live Activities, HealthKit, and more●FUNDING — Rork raised $2.8M from a16z, now drawing 743k+ monthly visits at an 85% growth rate●RN — Standard Rork generates iOS and Android apps together using React Native (Expo)●FOCUS — Rork focuses solely on native mobile apps, setting it apart from web-first Bolt and Lovable●PRICING — Free to start, paid plans from $25/mo, with Rork Max at $200/mo and two-click App Store publishing●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR scanning, Metal 3D, widgets, Live Activities, HealthKit, and more●FUNDING — Rork raised $2.8M from a16z, now drawing 743k+ monthly visits at an 85% growth rate●RN — Standard Rork generates iOS and Android apps together using React Native (Expo)●FOCUS — Rork focuses solely on native mobile apps, setting it apart from web-first Bolt and Lovable●PRICING — Free to start, paid plans from $25/mo, with Rork Max at $200/mo and two-click App Store publishing
Adding a keyboard toolbar to Rork text inputs — unifying iOS InputAccessoryView and an Android bar into one component
How to add a toolbar pinned above the keyboard — a Done button or quick-insert actions — to the React Native app Rork generates. The iOS InputAccessoryView and a hand-built Android bar, folded into one reusable component, with working code.
What I thought was "just drop one Done button above the input" turned into two entirely different implementations for iOS and Android, and half a day gone. I've shipped apps as an indie developer since 2014, and this came up when I added a small memo field to my wallpaper and healing apps so users could jot a line. On iOS, the dedicated InputAccessoryView worked immediately. Run the same code on Android and the toolbar never appears.
The official InputAccessoryView docs note, in small print, that it's iOS only. The form screens Rork generates don't paper over this gap, so the Android side is on you. Here's the path I took to fold both into a single reusable <KeyboardToolbar>, with the places I got stuck.
Why the "bar above the keyboard" is the part that doubles your work
A toolbar pinned above the keyboard is humble but genuinely useful. Japanese input drops uncommitted characters easily, so an explicit Done button that commits the IME composition before blurring noticeably reduces lost text. Quick-insert buttons (snippets, emoji, symbols) keep text-heavy screens flowing.
The catch is that iOS and Android produce this bar in completely different ways. The iOS InputAccessoryView is a special view the system physically attaches to the keyboard. When the keyboard rises, the bar rises with it; when it falls, they fall together. Android has no equivalent. KeyboardAvoidingView is a tool for pushing inputs above the keyboard, not for drawing a bar glued to it.
So iOS means "mount onto a dedicated component," and Android means "measure the keyboard height and position a bar absolutely yourself." Two designs to write separately, then reconcile into one look.
iOS: wiring InputAccessoryView to a nativeID
iOS is straightforward. Give the InputAccessoryView a unique nativeID, pass the same value to the TextInput's inputAccessoryViewID, and the bar snaps onto the keyboard whenever that input is focused.
Run this on an iOS device and a gray bar appears at the top of the keyboard the moment you tap the field; tapping Done dismisses it together with the keyboard. Keyboard.dismiss() blurs the field and commits any pending IME characters there.
Open the same MemoInputIOS on Android, though, and the contents of InputAccessoryView are never drawn. No error, no warning — so at first you waste time suspecting your own styles. Android needs a different foundation.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦If the iOS InputAccessoryView shows up but Android renders nothing, you'll be able to make both platforms match with a single component
✦You'll get a copy-paste cross-platform implementation that covers keyboard-height measurement, safe-area overlap, and sharing one bar across several inputs
✦You'll turn 'the Done button disappears on Android' and 'the bar overlaps the home indicator' into stable behavior using a measurement hook and a small branch
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Android has no InputAccessoryView — what to put in its place
To recreate the experience on Android, you find out "how many pixels of height the keyboard currently occupies" and position the bar absolutely, lifted from the bottom of the screen by exactly that amount. The key is React Native's Keyboard events. keyboardDidShow and keyboardDidHide (the did variants are reliable on Android) give you the keyboard height from event.endCoordinates.height.
Watch out for android:windowSoftInputMode. The Expo default (the config Rork outputs) leans toward adjustResize, which shrinks the root view by the keyboard height. In a design that places the bar "keyboardHeight above the bottom," adjustResize stacks with your offset and can fling the bar higher than intended. On Android I build the input screen assuming adjustPan-style behavior and deliberately avoid KeyboardAvoidingView on that one screen. Mixing them makes isolating the cause far harder.
A tiny hook to measure keyboard height
To confine the iOS/Android difference to one place, build a hook that returns only the keyboard's "height" and "is it visible." iOS uses keyboardWillShow (so the bar can track the animation smoothly); Android uses keyboardDidShow.
On Android, endCoordinates.height returns a value that includes the navigation (gesture) bar on some devices and not on others. If you settle the math without checking on real devices, you'll be off by a few pixels per model. I always run console.log(e.endCoordinates) once on a couple of devices in hand before deciding on the safe-area correction below.
Folding the two paths into one component
Now wrap the iOS-only route and the Android hand-built route into a <KeyboardToolbar> that looks identical to callers. Branch internally on Platform.OS, and let callers pass only a nativeID and the children (the buttons).
// KeyboardToolbar.tsximport React from "react";import { InputAccessoryView, Platform, View } from "react-native";import { useSafeAreaInsets } from "react-native-safe-area-context";import { useKeyboardHeight } from "./useKeyboardHeight";type Props = { nativeID: string; // ID to bind with the TextInput on iOS children: React.ReactNode; // bar contents (buttons)};const BAR_HEIGHT = 44;export function KeyboardToolbar({ nativeID, children }: Props) { const insets = useSafeAreaInsets(); // iOS: the system snaps it onto the keyboard for us if (Platform.OS === "ios") { return ( <InputAccessoryView nativeID={nativeID}> <View style={{ height: BAR_HEIGHT, ...barStyle }}>{children}</View> </InputAccessoryView> ); } // Android: lift by the keyboard height and position absolutely return <AndroidBar insets={insets}>{children}</AndroidBar>;}function AndroidBar({ insets, children }: { insets: { bottom: number }; children: React.ReactNode }) { const { keyboardHeight, isVisible } = useKeyboardHeight(); if (!isVisible) return null; // no keyboard, no bar // Some devices include the nav bar in endCoordinates. For those that don't, // add/subtract insets.bottom and confirm on real devices. const bottom = keyboardHeight; return ( <View pointerEvents="box-none" style={{ position: "absolute", left: 0, right: 0, bottom }} > <View style={{ height: BAR_HEIGHT, ...barStyle }}>{children}</View> </View> );}const barStyle = { flexDirection: "row" as const, justifyContent: "space-between" as const, alignItems: "center" as const, paddingHorizontal: 12, backgroundColor: "#f2f2f7", borderTopWidth: 0.5, borderTopColor: "#c6c6c8",};
The caller looks like this. On iOS inputAccessoryViewID takes effect; on Android KeyboardToolbar draws its own bar — so the screen's JSX stays as one.
Now iOS gets OS-managed snapping and Android gets measurement-based absolute positioning, two separate implementations the screen never has to think about. When you patch a form screen Rork generated after the fact, you just drop this one component next to the TextInput.
Resolving overlap with the safe area and home indicator
On Android gesture-navigation devices, whether endCoordinates.height includes the gesture bar varies. On devices where it doesn't, setting bottom: keyboardHeight directly leaves the bar overlapping the home indicator by a few pixels, making it hard to press. I keep insets.bottom from react-native-safe-area-context around and switch to bottom = keyboardHeight + insets.bottom only on the models where overlap actually shows up. Rather than forcing every device through one formula, splitting into "overlaps / doesn't overlap" held up better in the end.
On iOS, the system handles the safe area, so do not add insets.bottom inside InputAccessoryView. Adding it doubles the padding. Who owns the safe area differs by platform — that's the most confusing point here.
Sharing one bar across several inputs
On a screen with multiple inputs, writing a bar per field makes two bars flicker momentarily when focus moves. On iOS you can pass the same inputAccessoryViewID to several TextInputs and share one InputAccessoryView. To make the bar's buttons act on "the currently focused field," keep the active input in a ref.
import { useRef } from "react";import { TextInput } from "react-native";const focusedRef = useRef<TextInput | null>(null);// on each TextInput<TextInput inputAccessoryViewID={ID} onFocus={(e) => { focusedRef.current = e.target as unknown as TextInput; }}/>// from a bar button, act on focusedRef.current?.setNativeProps(...) etc.
On Android, one KeyboardToolbar on the screen is enough. useKeyboardHeight only watches the keyboard's visibility, so the same bar shows regardless of which field has focus. When you need to distinguish the target field, remember the active one in onFocus, the same as on iOS.
Four things that tripped me up in production
First, the interaction with autoFocus. Raising the keyboard with autoFocus on screen open meant, on Android, the layout settled before keyboardDidShow fired, so the bar failed to appear on the first open only. A short one-time delay right after mount, or dropping autoFocus for an explicit tap, stabilizes it.
Second, behavior inside modals. Using InputAccessoryView through a Modal produced cases on iOS where the bar hid behind the modal. Since I made input-bearing screens full-screen navigations and stopped putting text inputs inside Modal, this class of bug hasn't returned.
Third, pressing the bar closing the keyboard. Tapping the bar's Pressable can blur the TextInput, drop the keyboard, and make the Android bar vanish. I worked around it with pointerEvents handling on the Android bar and a design that keeps focus unless you explicitly call Keyboard.dismiss() (returning focus() to the input inside onPress).
Fourth, I tie measurement drift back to revenue when I verify. In my wallpaper and healing apps, longer stays on text-input screens lead to more subsequent navigation and a slightly higher chance of an AdMob impression. A bar that drifts per device and is hard to press quietly eats into that. I don't skip the unglamorous step of measuring on a few devices in hand before shipping, rather than locking the math to a single formula.
The next step
Start by dropping just the useKeyboardHeight hook into an existing text-input screen and console.log your device's endCoordinates once. Once you know whether the returned height includes the nav bar, the bottom formula collapses to two cases. iOS's InputAccessoryView is easy; the real foundation is this Android-side measurement.
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.