Minimal sticky header
Wordmark, centered links, and a cart badge that springs on change. Hairline appears on scroll.
blockminimalanycart-countsearch
$
npx shadcn@latest add @storekit/nav-minimalnav-minimal
Live and interactive. Switch the brand in the frame header to see the same source under another token file.
Installation
$
npx shadcn@latest add @storekit/nav-minimal$
pnpm dlx shadcn@latest add @storekit/nav-minimal$
yarn dlx shadcn@latest add @storekit/nav-minimal$
bunx --bun shadcn@latest add @storekit/nav-minimalSource
components/storekit/nav/nav-minimal.tsx
133 lines
"use client"
import { useEffect, useState } from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { Menu, Search, ShoppingBag, User } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"
/* Minimal sticky header: wordmark left, links centered, actions right. The
hairline under it only appears once the page has scrolled, and the cart
badge springs each time the count changes. Pass `scrollRoot` when the
header lives inside a scrolling element instead of the window. Below md the
links collapse behind a menu button that calls `onMenu` (pair it with a
drawer). */
type NavLink = { label: string; href: string }
const ring =
"outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper"
export function NavMinimal({
brandName = "Morrow",
markSrc,
links = [
{ label: "Shop", href: "#shop" },
{ label: "Bundles", href: "#bundles" },
{ label: "Lab results", href: "#lab-results" },
{ label: "Journal", href: "#journal" },
],
cartCount = 0,
onSearch,
onAccount,
onCart,
onMenu,
scrollRoot,
className,
}: {
brandName?: string
markSrc?: string
links?: NavLink[]
cartCount?: number
onSearch?: () => void
onAccount?: () => void
onCart?: () => void
onMenu?: () => void
scrollRoot?: HTMLElement | null
className?: string
}) {
const reduce = useReducedMotion()
const sk = useSkBlock("nav", "nav-minimal")
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const target: HTMLElement | Window = scrollRoot ?? window
const read = () => setScrolled((scrollRoot ? scrollRoot.scrollTop : window.scrollY) > 4)
read()
target.addEventListener("scroll", read, { passive: true })
return () => target.removeEventListener("scroll", read)
}, [scrollRoot])
const icon = cn("grid h-11 w-11 place-items-center rounded-sk-btn text-sk-ink transition-colors duration-200 hover:bg-sk-ink/[0.05] active:scale-[0.97]", ring)
return (
<header
ref={sk.ref}
{...sk.attrs}
className={cn(
"sticky top-0 z-30 border-b bg-sk-paper/95 transition-colors duration-300",
scrolled ? "border-sk-line" : "border-transparent",
className
)}
>
<div className="mx-auto flex h-16 max-w-6xl items-center gap-2 px-2 sm:gap-4 sm:px-8">
<button type="button" aria-label="Open menu" aria-haspopup="dialog" onClick={onMenu} className={cn(icon, "md:hidden")}>
<Menu size={20} aria-hidden />
</button>
<a href="/" className={cn("flex min-h-11 min-w-0 items-center gap-2.5 rounded-sk-btn pr-1", ring)}>
{markSrc ? <img src={markSrc} alt="" className="h-7 w-7 shrink-0 object-contain" /> : null}
<span className="truncate font-sk-display text-[15px] font-semibold tracking-[-0.02em] text-sk-ink">{brandName}</span>
</a>
<nav aria-label="Primary" className="hidden flex-1 justify-center md:flex">
<ul className="flex items-center gap-1">
{links.map((l) => (
<li key={l.href}>
<a
href={l.href}
className={cn("inline-flex h-11 items-center rounded-sk-btn px-3 text-sm text-sk-muted transition-colors duration-200 hover:text-sk-ink", ring)}
>
{l.label}
</a>
</li>
))}
</ul>
</nav>
<div className="ml-auto flex shrink-0 items-center md:ml-0">
<button type="button" aria-label="Search" onClick={onSearch} className={icon}>
<Search size={18} aria-hidden />
</button>
<button type="button" aria-label="Account" onClick={onAccount} className={cn(icon, "hidden sm:grid")}>
<User size={18} aria-hidden />
</button>
<button
type="button"
aria-label={`Cart, ${cartCount} ${cartCount === 1 ? "item" : "items"}`}
onClick={() => {
sk.emit("interact", "cart_open")
onCart?.()
}}
className={cn(icon, "relative")}
>
<ShoppingBag size={18} aria-hidden />
<AnimatePresence initial={false}>
{cartCount > 0 ? (
<motion.span
key={cartCount}
aria-hidden
initial={reduce ? { opacity: 0 } : { scale: 0.4, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: reduce ? 1 : 0.4, opacity: 0, transition: { duration: 0.12 } }}
transition={reduce ? { duration: 0.15 } : { type: "spring", stiffness: 520, damping: 16 }}
className="absolute right-1 top-1 grid h-[18px] min-w-[18px] place-items-center rounded-full bg-sk-accent px-1 text-[10px] font-semibold tabular-nums leading-none text-sk-accent-ink"
>
{cartCount > 99 ? "99+" : cartCount}
</motion.span>
) : null}
</AnimatePresence>
</button>
</div>
</div>
</header>
)
}
Usage
Pass scrollRoot when the header sits inside a scrolling element rather than the window.






