Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Mobile UI in React
  4. Web App That Feels Native on a Phone: 12 React Fixes
Auf Deutsch lesen
  1. Blog
  2. Mobile UI in React
  3. Web App That Feels Native on a Phone: 12 React Fixes
Mobile UI in React
Mobile UI in React

Web App That Feels Native on a Phone: 12 React Fixes

SR
Serban Rusu · Founder of Wingo UI
Oct 13, 2026 · 11 min read

On this page

0%
  1. What makes a web app feel native on a phone?
  2. 1. Does it launch like an app?
  3. 2. Does a tap answer at once?
  4. 3. Does focusing a field zoom the page?
  5. 4. Does the interface act like a document?
  6. 5. Is the navigation at the thumb?
  7. 6. Does the title behave like iOS?
  8. 7. Can you swipe between tabs?
  9. 8. Do menus open as action sheets?
  10. 9. Do pickers and forms open from the bottom?
  11. 10. Can you pull to refresh?
  12. 11. Do live events arrive like notifications?
  13. 12. What about haptics, the keyboard and motion?
  14. Which of these should you add first?
  15. FAQ
    1. How do I make a PWA feel native?
    2. Can a web app use haptic feedback on iPhone?
    3. Should a web app copy iOS design on Android?
    4. What gives away that an app is a website?
    5. Are these components free?

TL;DR

A web app that feels native on a phone gets the behavior right before the looks. It launches standalone and edge to edge, answers a tap at once without a gray flash or a double-tap zoom, opens menus and pickers as sheets from the bottom, lets you swipe between tabs and pull to refresh, and keeps the composer above the keyboard. Large titles, in-app banners and haptics come last, and each needs a visible fallback because browser support for them is uneven.

Published Oct 13, 2026

You built the app, added it to your home screen, and it still feels like a website: links flash gray when you tap them, a double tap on a button zooms the page, the menu opens as a tiny popover under your thumb, and the keyboard hides the message box. A web app that feels native gets a dozen small behaviors right, and most of them are free CSS or a few props. This list walks through twelve of them, from the launch to the keyboard, each with the component we built for it in Wingo UI, the code, and what a browser still cannot do.

The list is the practical companion to Mobile-first React components, which covers the layout rules behind it. More guides live in Mobile UI in React.

What makes a web app feel native on a phone?

Behavior first, looks second. Most advice on how to make a PWA feel native stops at a manifest and a few meta tags, which is step one of twelve. People forgive a web app that does not look exactly like Settings on an iPhone. They notice at once when a tap waits, a gesture does nothing, or the keyboard covers what they type. Web apps that feel native fix those first, so the twelve details below start with the ones every app needs and end with the polish.

#DetailTool
1Launch standalone, edge to edgemanifest, viewport-fit=cover, safe area insets
2Taps that answer at oncetouch-manipulation, tap highlight, press state
3Inputs that never zoom the page16px text on phones
4Chrome that does not act like a documentselect-none, overscroll-behavior
5Navigation at the thumbBottom Tab Bar
6A large title that hands over to the barLarge Title Header
7Tabs you can swipeSwipeable Tabs
8Action sheets instead of dropdownsAction Sheet
9Bottom sheets for everything elseDrawer
10Pull to refreshPull To Refresh
11In-app banners for live eventsNotification Banner
12Haptics, a keyboard-aware composer, and reduced motionHaptic Button, useKeyboardInset

1. Does it launch like an app?

An installed app opens without the address bar and fills the screen. That takes a manifest with display: "standalone" and a viewport that reaches the edges. In the Next.js App Router both are files and exports:

tsx
// app/manifest.ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Northwind CRM",
short_name: "Northwind",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#ffffff",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}
tsx
// app/layout.tsx
import type { Viewport } from "next";
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
};

With viewportFit: "cover" the page draws under the notch and the home indicator, so every bar pinned to an edge must pad itself with env(safe-area-inset-*). Our Tailwind safe area guide has the utilities and the arithmetic for stacked bars. Some details only make sense once installed, like hiding your own install banner. A custom variant keeps that in CSS:

css
/* app/globals.css */
@custom-variant standalone (@media (display-mode: standalone));

Then standalone:hidden hides an element inside the installed app. The React PWA install prompt guide covers how people get there on Chrome, Edge and iPhone.

2. Does a tap answer at once?

On a native control the press shows under your finger, with no gray box and no wait. Two CSS details get a web button there. touch-action: manipulation turns off double-tap zoom on the element, so a quick second tap on a stepper never zooms the page. WebKit's post on fast tapping (opens in a new tab) (December 2015) explains that it also keeps single taps immediate at every zoom level. And -webkit-tap-highlight-color: transparent removes the translucent box Safari and Chrome draw over a tapped link. Tailwind CSS v4's preflight already sets it to transparent on html, so in a Tailwind app the job is the opposite one: give every pressable its own pressed state.

tsx
<button
type="button"
className="h-11 touch-manipulation rounded-xl bg-foreground px-4 text-background transition-transform [-webkit-tap-highlight-color:transparent] active:scale-[0.97]"
>
Save
</button>

The free Wingo UI Button carries both classes itself, so a copied button keeps them without a global stylesheet, and presses with a spring instead of a CSS transition. Our Tailwind touch-manipulation tutorial goes through every touch-* value and the bug each one fixes.

3. Does focusing a field zoom the page?

Safari on iOS zooms into a focused field whose text is smaller than 16px, and it does not zoom back out when the field loses focus. Adding maximum-scale=1 to the viewport stops that zoom, but Android browsers then block pinch zoom for the whole page, and Lighthouse fails the page on its viewport accessibility audit. Use 16px text on phones and your smaller size from md up:

tsx
<input aria-label="Search clients" className="h-11 w-full rounded-xl bg-foreground/5 px-4 text-base md:h-10 md:text-sm" />

The Wingo UI design system makes this one rule for every text field, text-base md:text-sm, and the shared field box and the Prompt Input follow it. Watch for the same bug in places that are easy to miss: a search field inside a header, a select, and a contenteditable editor all zoom the page when their computed font size is under 16px.

4. Does the interface act like a document?

Only where it should. A web page lets you select any text and chains a scroll from an inner list into the page behind it. An app does neither on its controls. Add select-none to buttons, tabs and bars, so a long press on a tab does not select its label, and leave real content selectable: people copy addresses, order numbers and error messages. Put overscroll-behavior: contain (overscroll-contain) on inner scrollers, so the end of a list inside a sheet does not scroll the page behind it; Safari supports it from version 16. Long press callouts on images in your chrome go away with [-webkit-touch-callout:none], which Safari supports and other browsers ignore.

5. Is the navigation at the thumb?

Native apps put their 3 to 5 main sections in a bar at the bottom, where the thumb already is. On the web that bar needs the home indicator inset, a hide-on-scroll behavior and a way to tell the rest of the page how tall it is. The Bottom Tab Bar does those three, and our React bottom navigation bar guide builds one step by step. Keep a sidebar from md up: a tab bar on a 1440px screen looks like a phone app stretched to fit.

6. Does the title behave like iOS?

The iOS large title is the most recognizable detail of an iPhone app: a 34px heading that scrolls up with the page and hands over to a small title in a frosted bar. The Large Title Header follows your finger through that handover, settles to fully expanded or fully collapsed when you stop halfway, and stretches the title by up to 8% when you pull past the top in Safari. The title stays the page's only h1, and under reduced motion the handover becomes an instant switch. The React collapsing header guide builds this handover, and a photo hero, on motion values.

tsx
"use client";
import { useState } from "react";
import { SquarePen } from "lucide-react";
import { Button } from "@/components/ui/button";
import { LargeTitleHeader } from "@/components/ui/large-title-header";
export function MessagesHeader() {
const [query, setQuery] = useState("");
return (
<LargeTitleHeader
title="Messages"
subtitle="3 unread"
backLabel="Settings"
backHref="/settings"
search
searchValue={query}
onSearchChange={setQuery}
actions={
<Button variant="ghost" size="icon" radius="full" aria-label="New message">
<SquarePen />
</Button>
}
/>
);
}
Scroll the conversations: the large title slides up and the compact title rises into the glass bar. Tap Search and the field moves right under the bar with a Cancel button
$ npx wingo-ui@latest add large-title-header
ProLarge Title Header docs

Use it on top-level lists and settings pages. Detail screens read better with mode="compact", which shows only the bar, the way iOS does on a pushed page.

7. Can you swipe between tabs?

On a phone, tabs are pages side by side, and a sideways swipe moves between them. Plain tabs that only react to taps feel like a website. The Swipeable Tabs put the pages in a native scroll snap row, so the swipe has the browser's own momentum, and the underline stretches between tabs as the pages move. A vertical scroll inside a page never changes the tab, and tapping the active tab scrolls its page back to the top. The React swipeable tabs guide covers the scroll snap track and the indicator in detail.

tsx
import { SwipeableTabs } from "@/components/ui/swipeable-tabs";
import { ClientList } from "./client-list"; // your own list
export function ClientTabs() {
return (
<SwipeableTabs
aria-label="Clients"
options={[
{ value: "all", label: "All", badge: 4, content: <ClientList /> },
{ value: "active", label: "Active", badge: 2, content: <ClientList status="active" /> },
{ value: "overdue", label: "Overdue", badge: 1, content: <ClientList status="overdue" /> },
]}
/>
);
}
Swipe the client list sideways, or swipe with two fingers on a trackpad: the indicator follows the pages, and a tap on a tab slides to it
$ npx wingo-ui@latest add swipeable-tabs
ProSwipeable Tabs docs

Swiping is never the only way: every tab stays a button, and the arrow keys move between them. Pages mount lazily and stay mounted by default (lazy and keepMounted), so a page you swipe back to is not mounted again from scratch.

8. Do menus open as action sheets?

On a phone they should. Tap the "more" button in a native app and a sheet of actions rises from the bottom, with a red destructive row and a separate Cancel. A dropdown anchored to a 44px button covers the thumb that opened it. The Action Sheet always opens as a sheet on phones; from 768px it becomes a compact dialog by default, or a menu anchored to the trigger with desktop="popover". The React action sheet guide covers async rows and what happens when an action fails. It can also be awaited, which keeps the call site short:

tsx
"use client";
import { MoreHorizontal } from "lucide-react";
import { useActionSheet } from "@/components/ui/action-sheet";
import { Button } from "@/components/ui/button";
type PhotoMenuProps = { onSave: () => void; onDelete: () => Promise<void> };
export function PhotoMenu({ onSave, onDelete }: PhotoMenuProps) {
const actionSheet = useActionSheet();
async function open() {
const id = await actionSheet.show({
actions: [
{ id: "save", label: "Save to photos" },
{ id: "delete", label: "Delete for me", tone: "danger", onSelect: onDelete },
],
});
if (id === "save") onSave();
}
return (
<Button variant="ghost" size="icon" radius="full" aria-label="Photo options" onClick={open}>
<MoreHorizontal />
</Button>
);
}

show() needs an ActionSheetProvider near the root (it throws without one) and resolves with the chosen id, or null on cancel. An action that returns a promise shows a spinner in its row until it settles; if the promise rejects, the row shakes and stays open for a retry.

Tap the listing's menu button: the sheet rises with the actions and a separate Cancel. Try Delete listing, which spins until its promise resolves
$ npx wingo-ui@latest add action-sheet
ProAction Sheet docs

Set variant="material" for one attached sheet with a handle and icon rows, closer to Android.

9. Do pickers and forms open from the bottom?

Everything that would float over the page on a desktop, such as filters, a date picker, a share menu or a short form, should rise from the bottom on a phone, where the thumb can reach it and drag it away. The free Drawer is the sheet the rest of the library uses: drag to dismiss, snap points, a scroll body that drags the sheet only once its list is back at the top, and from 768px a centered sheet, a side panel or a dialog, set with desktop. The React bottom sheet guide covers snap points and the keyboard in detail.

10. Can you pull to refresh?

In an installed PWA, only if you add the gesture yourself. People pull a list down to reload it without thinking, and in a standalone PWA the browser's own pull to refresh is gone or reloads the whole page. The Pull To Refresh adds the gesture with rubberband resistance and an arrow, iOS spokes or a Material ring:

tsx
import { PullToRefresh } from "@/components/ui/pull-to-refresh";
import { InboxList } from "./inbox-list"; // your own list
export function Inbox({ reload }: { reload: () => Promise<void> }) {
return (
<PullToRefresh scroll="self" className="min-h-0 flex-1" onRefresh={reload}>
<InboxList />
</PullToRefresh>
);
}

It sets overscroll-behavior-y: contain on the scroller so Chrome does not reload the page, ignores sideways swipes, never starts a pull inside an input or a nested list that is not at its top, and spins for at least 500ms so the indicator never flashes. There is no keyboard gesture, so keep a reload button, or a shortcut, for mouse and keyboard users. The React pull to refresh guide explains why Safari needs a non-passive touchmove listener for this.

11. Do live events arrive like notifications?

A toast at the bottom suits "Saved". A new message, an order or a reminder that arrives while someone uses the app reads better as a banner from the top, like the system's own. The Notification Banner queues them one at a time, lets you swipe a banner up to dismiss it or pull it down to expand its actions, and pauses its timer while you hold it:

tsx
"use client";
import { useRouter } from "next/navigation";
import { MessageCircle } from "lucide-react";
import { notify } from "@/components/ui/notification-banner";
export function useMessageBanner() {
const router = useRouter();
return (message: { id: string; from: string; text: string }) =>
notify({
id: `message-${message.id}`,
title: message.from,
message: message.text,
icon: <MessageCircle />,
onPress: () => router.push(`/messages/${message.id}`),
});
}

Mount <NotificationBanner appName="Northwind CRM" /> once near the root. Sending the same id again updates the banner in place instead of stacking a duplicate, which matters when a socket delivers the same event twice. This is an in-app banner; it does not replace Web Push for people who have closed the app.

12. What about haptics, the keyboard and motion?

Three last details, each with a limit worth knowing.

Haptics. The Haptic Button presses on a spring, spreads a ripple from the touch point and plays a light tick on touch, and its mode="hold" fills while you hold to confirm a delete. The tick uses navigator.vibrate, which works in Chromium on Android after the first tap. Safari on iPhone has no Vibration API, and since iOS 26.5 a web page can no longer trigger the system tick of a hidden switch, so on a current iPhone the press and the fill carry the feedback alone. Our Web Vibration API guide covers what still ticks.

The keyboard. On iOS, and on Android since Chrome 108, the keyboard covers fixed bars instead of pushing them up. A chat composer pinned to the bottom needs to follow it. The useKeyboardInset hook reports the covered height as a motion value, and the iOS Safari keyboard guide builds the composer step by step.

Motion. Native transitions use springs and respect the system's reduced motion setting. The components in this post take their springs from one shared set in lib/motion and read the reduced motion preference, so the title handover becomes an instant switch, the action sheet fades instead of sliding, and the haptic button drops its dip, ripple and shake.

Which of these should you add first?

Start with the ones that cost nothing, because they fix the tells everyone notices: the launch (1), the taps (2), the zoom (3) and the document behavior (4) are CSS and a manifest. Then the sheets: npx wingo-ui@latest add drawer installs the free Drawer. The bottom tab bar, large title, swipeable tabs, action sheet, pull to refresh, banners, haptic button and keyboard hook are part of Wingo UI Pro, with live demos on each component page, so you can try the native app feel on your phone before you install them.

Components in this post

  • Large Title Header

    A React iOS large title header that shrinks into a compact title in a glass bar as you scroll.

    Pro
  • Action Sheet

    A React iOS-style action sheet: a short list of actions with a title, red destructive rows, a separate Cancel and an awaitable show().

    Pro
  • Notification Banner

    iOS-style React notification banners for messages, orders and reminders that drop from the top one at a time, with a queue.

    Pro
  • Swipeable Tabs

    React swipeable tabs whose pages sit side by side and follow your thumb: swipe and the indicator tracks the finger, tap and the pages slide.

    Pro
  • Haptic Button

    A React haptic button that feels native: spring press, touch ripple, a haptic tick, a long-press action and a hold-to-confirm mode.

    Pro
  • Pull To Refresh

    A React pull to refresh wrapper: pull a list past its top to reload it, with rubberband resistance, iOS spokes or a Material ring, haptics.

    Pro

FAQ

How do I make a PWA feel native?

Launch it with display standalone and viewport-fit=cover, pad every pinned bar with the safe area insets, and fix the behaviors that give a web page away: slow or flashing taps, dropdown menus where a phone expects a sheet, fixed bars hidden by the keyboard, and pages that scroll like documents. Visual details like large titles come after that.

Can a web app use haptic feedback on iPhone?

Barely. Safari has no Vibration API, and since iOS 26.5 the system tick of a native switch plays only when the user's own tap toggles it, so a web app cannot tick on demand. Android Chrome supports navigator.vibrate after the first tap, so treat haptics as an extra and keep a visual response for every one.

Should a web app copy iOS design on Android?

Copy the behaviors both platforms share, such as sheets from the bottom, tabs you can swipe and instant press feedback. Platform-specific looks are a choice: several Wingo UI components offer an iOS and a Material variant, for example the action sheet and the pull to refresh indicator.

What gives away that an app is a website?

The usual tells are a gray box over tapped links, a page that zooms when you double tap a button or focus a small input, menus that open as tiny popovers, the keyboard covering a bottom bar, text that gets selected when you hold a button, and the whole page bouncing behind a scrolling list.

Are these components free?

The Button and the Drawer, which carry the tap and sheet basics, are free in Wingo UI. The Bottom Tab Bar, Large Title Header, Swipeable Tabs, Action Sheet, Pull To Refresh, Notification Banner, Haptic Button and the useKeyboardInset hook are part of Wingo UI Pro.

  • Mobile UI
  • PWA
  • iOS
  • React
  • Native feel

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Mobile UI in React

Mobile-First React Components: 9 Rules With Code

Mobile-first React components, rule by rule: bottom sheets instead of popovers, 44px touch targets, safe areas, dvh, keyboard-aware bars and tables as cards.

SRSerban Rusu·Oct 9, 2026·21 min read
Mobile UI in React

React Action Sheet: iOS-Style, Async and Responsive

Build a React action sheet like iOS: red destructive rows, a separate Cancel, an awaitable show() that waits for async actions, and a dialog from 768px.

SRSerban Rusu·Oct 16, 2026·9 min read
Mobile UI in React

React Collapsing Header: iOS Large Title and Photo Hero

Build a react collapsing header: the iOS large title handover and a parallax photo hero, driven by scroll motion values instead of a re-render per frame.

SRSerban Rusu·Oct 16, 2026·8 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI