--- name: ios-mobile-first description: Single source of truth for iOS-native mobile web UX. Covers viewport units, safe areas, Dialog-to-Drawer conversion, Tabs-to-vertical-stacking, touch targets, input zoom prevention, and responsive patterns. Use when building any UI component, fixing mobile issues, reviewing responsive code, or when mobile UX is mentioned. --- # iOS Mobile-First Design > **Official guide:** `~/.arman/rules/nextjs-best-practices/nextjs-guide.md` — See the official guide for core architecture. The old §13 (Mobile-First) was removed; mobile-first patterns in this file will move to a dedicated *Mobile & Responsive UX* guide. Single source of truth for mobile UX. Desktop stays unchanged; mobile gets iOS-native treatment. **Reference implementations:** `components/layout/FeedbackButton.tsx`, `components/admin/McpToolsManager.tsx`, `components/admin/ToolUiComponentEditor.tsx` --- ## Golden Rules 1. **Always `dvh`** — never `vh` or `h-screen` 2. **Always `pb-safe`** — on fixed bottom elements 3. **Always 16px inputs** — prevents iOS zoom (`text-base` + `style={{ fontSize: '16px' }}`) 4. **Always 44pt touch targets** — minimum `h-10 w-10` 5. **Always `--header-height`** — never hardcode 6. **Always Drawer on mobile** — never Dialog 7. **Never tabs on mobile** — stack vertically 8. **Never nested scrolling** — single scroll area per view 9. **Always test iOS Safari** — on real device --- ## Viewport & Layout ### Dynamic Viewport Units ```tsx // ✅ Adapts to mobile browser chrome
// ❌ Breaks when browser chrome hides/shows
``` ### Safe Areas ```tsx // ✅ Respects iPhone home indicator / notch
// Utilities in globals.css: // .pb-safe { padding-bottom: env(safe-area-inset-bottom, 1rem); } // .mb-safe { margin-bottom: env(safe-area-inset-bottom, 1rem); } ``` ### Header Height ```tsx // ✅ Uses CSS variable (--header-height: 2.5rem)
// ❌ Hardcoded
``` ### Page Layouts ```tsx // Full-height page below header
{/* Scrollable content */}
// With fixed bottom bar
{/* Content */}
{/* Actions */}
// Standard scrollable page
{/* Content */}
``` --- ## MANDATORY: Dialog = Desktop, Drawer = Mobile **Every dialog/modal MUST use `useIsMobile()` for conditional rendering.** ```tsx import { useIsMobile } from "@/hooks/use-mobile"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer"; function MyComponent() { const isMobile = useIsMobile(); const [isOpen, setIsOpen] = useState(false); if (isMobile) { return ( Title
{/* Content — single scroll area, no nesting */}
); } return ( Title
{/* Content */}
); } ``` | Element | Mobile (Drawer) | Desktop (Dialog) | |---------|----------------|-----------------| | Max height | `max-h-[85dvh]` | `max-h-[90dvh]` | | Max width | Full width | `max-w-[95vw]` / `lg:max-w-[1400px]` | | Scroll | `overflow-y-auto overscroll-contain` | `overflow-y-auto` | | Safe area | `pb-safe` | Not needed | | Layout | Natural flow | `flex flex-col overflow-hidden` | --- ## MANDATORY: Tabs = Desktop Only **Never use tabs on mobile.** They cause UX friction, nested scroll trapping, and hidden content. Stack all sections vertically with visual dividers: ```tsx import { useIsMobile } from "@/hooks/use-mobile"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; function MyForm() { const isMobile = useIsMobile(); const [activeTab, setActiveTab] = useState("basic"); if (isMobile) { return (
{/* Section 1 */}

Basic Info

{/* Fields */}
{/* Section 2 */}

Advanced

{/* Fields */}
{/* Full-width actions */}
); } return ( Basic Info Advanced {/* Fields */} {/* Fields */} ); } ``` **Mobile stacking features:** accent bars (`h-6 w-1 bg-primary`), border separators, single scroll area, full-width buttons, `pb-safe`. --- ## iOS Zoom Prevention ```tsx // ✅ All inputs MUST have ≥16px font size