Phone-width view dock
A bottom nav dock built from measured target sizes, not guessed ones
npx shadcn@latest add https://ethankd.com/r/mobile-dock.json"use client";
import { useState } from "react";
import { LayoutGrid, Gauge, Map, Search, Settings } from "lucide-react";
const ITEMS = [
{ id: "overview", label: "Overview", Icon: LayoutGrid },
{ id: "performance", label: "Performance", Icon: Gauge },
{ id: "map", label: "Map", Icon: Map },
{ id: "seo", label: "SEO", Icon: Search },
{ id: "settings", label: "Settings", Icon: Settings },
];
/**
* Phone-width view nav. A dock, not a scrolling tab strip: every item is on
* screen and reachable, and the active item is the one place colour carries
* meaning — everywhere else it would just be decoration. Below the width
* where a label and a 44px target both fit, the label drops to sr-only
* rather than the target shrinking; `aria-current` plus the page's own
* heading carry the state that colour alone no longer can.
*/
export function MobileDock() {
const [active, setActive] = useState("overview");
return (
<nav aria-label="Views" className="flex justify-center">
<ul className="flex max-w-full items-center gap-1 rounded-[1.25rem] border border-black/10 bg-white p-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.04),0_8px_24px_rgba(0,0,0,0.06)]">
{ITEMS.map(({ id, label, Icon }) => {
const current = id === active;
return (
<li key={id}>
<button
type="button"
onClick={() => setActive(id)}
aria-current={current ? "page" : undefined}
className="grid h-11 min-w-11 grid-flow-col place-items-center gap-1.5 rounded-xl px-3 transition-[background-color,color] duration-200 ease-out"
style={
current
? { background: "#093538", color: "#fff" }
: { color: "#6b7280" }
}
>
<Icon aria-hidden size={18} />
<span className={current ? "hidden text-[13px] font-medium min-[420px]:inline" : "sr-only"}>
{label}
</span>
</button>
</li>
);
})}
</ul>
</nav>
);
}