# iOS Maps

> A vector-style map with a three-detent bottom sheet and place card.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/templates/ios-maps
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/ios-maps.json
```

Adds the page at `app/ios-maps/page.tsx` together with the components below. Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Components

- [iOS Kit](https://chromotion.dev/r/ios-kit.md): `@/components/ios-kit`

## Dependencies

```bash
npm install lucide-react
```

## Source

```tsx title="app/ios-maps/page.tsx"
"use client"

import * as React from "react"
import { Briefcase, Car, Coffee, Home, Layers, MapPin, Navigation, Search, ShoppingBag, Star, Utensils } from "lucide-react"

import { cn } from "@/lib/utils"
import { IOSDevice } from "@/components/ios-kit"

const detents = ["peek", "half", "full"] as const
type Detent = (typeof detents)[number]
const height: Record<Detent, string> = { peek: "142px", half: "52%", full: "88%" }

const favorites = [
  { label: "Home", icon: Home, color: "bg-(color:--ios-blue)" },
  { label: "Work", icon: Briefcase, color: "bg-(color:--ios-orange)" },
  { label: "Coffee", icon: Coffee, color: "bg-(color:--ios-red)" },
  { label: "Gym", icon: Star, color: "bg-(color:--ios-green)" },
]

const recents = [
  { name: "Ferry Building Marketplace", detail: "1 Ferry Building, San Francisco", icon: ShoppingBag, eta: "12 min" },
  { name: "Tartine Bakery", detail: "600 Guerrero St", icon: Utensils, eta: "9 min" },
  { name: "Dolores Park", detail: "Dolores St & 19th St", icon: MapPin, eta: "7 min" },
  { name: "Blue Bottle Coffee", detail: "66 Mint St", icon: Coffee, eta: "5 min" },
]

export default function IOSMaps() {
  const [detent, setDetent] = React.useState<Detent>("half")
  const [place, setPlace] = React.useState<(typeof recents)[number] | null>(null)

  const cycle = () => setDetent((current) => detents[(detents.indexOf(current) + 1) % detents.length])

  return (
    <IOSDevice className="bg-[#eae6dd]">
      <svg aria-hidden viewBox="0 0 390 844" preserveAspectRatio="xMidYMid slice" className="absolute inset-0 size-full">
        <rect width="390" height="844" fill="#ebe7df" />
        <path d="M0 0h390v190c-60 30-120-10-190 20S60 260 0 230Z" fill="#9fd0f2" />
        <path d="M30 330c40-30 110-20 130 20s-10 90-60 100-100-30-70-120Z" fill="#c8e6b4" />
        <path d="M240 520c50-20 110 10 120 60s-30 80-80 70-90-90-40-130Z" fill="#c8e6b4" />
        <g fill="none" strokeLinecap="round" stroke="#fff" strokeWidth="12">
          <path d="M-20 420 L420 300" />
          <path d="M120 230 L200 900" />
          <path d="M-20 640 L420 560" />
          <path d="M300 220 L260 900" />
        </g>
        <g fill="none" strokeLinecap="round" stroke="#fff" strokeWidth="6">
          <path d="M-20 520 L420 440" />
          <path d="M40 260 L90 900" />
          <path d="M360 240 L330 900" />
        </g>
        <path d="M-20 420 L420 300" fill="none" stroke="#f7c35f" strokeWidth="3" strokeLinecap="round" opacity="0.8" />
      </svg>

      <div className="absolute top-[60px] right-3 z-20 flex flex-col overflow-hidden rounded-[12px] bg-white/85 shadow-md backdrop-blur-xl dark:bg-zinc-800/85">
        {[{ label: "Map layers", icon: Layers }, { label: "My location", icon: Navigation }].map(({ label, icon: Icon }) => (
          <button key={label} type="button" aria-label={label} className="flex size-11 items-center justify-center text-(color:--ios-label) not-last:border-b not-last:border-(color:--ios-sep) active:bg-black/10">
            <Icon className="size-5" />
          </button>
        ))}
      </div>

      <div aria-hidden className="absolute top-[41%] left-[46%] z-10">
        <span className="absolute -inset-3 animate-ping rounded-full bg-(color:--ios-blue)/30" />
        <span className="block size-[18px] rounded-full border-[3px] border-white bg-(color:--ios-blue) shadow-md" />
      </div>
      {place && (
        <div className="absolute top-[24%] left-[58%] z-10 flex -translate-x-1/2 flex-col items-center">
          <span className="flex size-9 items-center justify-center rounded-full bg-(color:--ios-red) text-white shadow-lg">
            <MapPin className="size-5" />
          </span>
          <span className="mt-1 max-w-32 truncate rounded-md bg-white/90 px-1.5 py-0.5 text-[11px] font-medium text-black">{place.name}</span>
        </div>
      )}

      <section
        aria-label="Search and places"
        style={{ height: place ? "190px" : height[detent] }}
        className="absolute inset-x-0 bottom-0 z-30 flex flex-col overflow-hidden rounded-t-[30px] bg-(color:--ios-bg)/90 shadow-[0_-8px_30px_rgb(0_0_0/0.18)] backdrop-blur-2xl transition-[height] duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]"
      >
        <button type="button" aria-label={`Resize sheet (now ${detent})`} onClick={cycle} className="flex h-6 shrink-0 items-center justify-center">
          <span className="h-[5px] w-9 rounded-full bg-(color:--ios-label3)" />
        </button>

        {place ? (
          <div className="px-5 pt-1">
            <div className="flex items-start justify-between gap-3">
              <div className="min-w-0">
                <h2 className="truncate text-[22px] leading-7 font-bold">{place.name}</h2>
                <p className="text-[15px] text-(color:--ios-label2)">{place.detail}</p>
              </div>
              <button type="button" onClick={() => setPlace(null)} aria-label="Close" className="flex size-7 shrink-0 items-center justify-center rounded-full bg-(color:--ios-fill) text-(color:--ios-label2)">
                ×
              </button>
            </div>
            <button type="button" className="mt-4 flex w-full items-center justify-center gap-2 rounded-[14px] bg-(color:--ios-blue) py-3 text-[17px] font-semibold text-white active:opacity-70">
              <Car className="size-5" /> Directions · {place.eta}
            </button>
          </div>
        ) : (
          <div className="min-h-0 flex-1 overflow-y-auto px-4 pb-10 [scrollbar-width:none]">
            <label className="flex h-11 items-center gap-2 rounded-[12px] bg-(color:--ios-fill) px-3 text-(color:--ios-label2)">
              <Search className="size-[18px]" />
              <input
                placeholder="Search Maps"
                aria-label="Search Maps"
                onFocus={() => setDetent("full")}
                className="min-w-0 flex-1 bg-transparent text-[17px] text-(color:--ios-label) outline-none placeholder:text-(color:--ios-label2)"
              />
            </label>

            <h2 className="mt-4 mb-2 px-1 text-[20px] font-bold">Favorites</h2>
            <ul className="flex justify-between px-1">
              {favorites.map(({ label, icon: Icon, color }) => (
                <li key={label}>
                  <button type="button" className="flex w-16 flex-col items-center gap-1.5 active:opacity-60">
                    <span className={cn("flex size-[50px] items-center justify-center rounded-full text-white", color)}>
                      <Icon className="size-6" />
                    </span>
                    <span className="text-[12px]">{label}</span>
                  </button>
                </li>
              ))}
            </ul>

            <h2 className="mt-5 mb-2 px-1 text-[20px] font-bold">Recents</h2>
            <ul className="overflow-hidden rounded-[16px] bg-(color:--ios-surface)">
              {recents.map((item) => (
                <li key={item.name}>
                  <button
                    type="button"
                    onClick={() => setPlace(item)}
                    className="relative flex w-full items-center gap-3 px-4 py-2.5 text-left after:absolute after:right-0 after:bottom-0 after:left-[60px] after:h-px after:bg-(color:--ios-sep) active:bg-(color:--ios-fill)"
                  >
                    <span className="flex size-8 items-center justify-center rounded-full bg-(color:--ios-fill) text-(color:--ios-label2)">
                      <item.icon className="size-4" />
                    </span>
                    <span className="min-w-0">
                      <span className="block truncate text-[17px] leading-[22px]">{item.name}</span>
                      <span className="block truncate text-[14px] text-(color:--ios-label2)">{item.detail}</span>
                    </span>
                  </button>
                </li>
              ))}
            </ul>
          </div>
        )}
      </section>
    </IOSDevice>
  )
}
```
