Store

Minimal Store Header

Client store header with a centered mega menu of link-only columns, search and bag count, collapsing to a left Sheet with accordion categories when narrow. No announcement bar.

Free, MIT licenseOpen in Browse
"use client"

import {
  BasketIcon,
  CircleIcon,
  ListIcon,
  MagnifyingGlassIcon,
} from "@phosphor-icons/react"
import { cn } from "cn"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
import { Button, buttonVariants } from "@/components/ui/button"
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

// The copy: change text here, not in the markup.
const COPY = {
  brand: "Orbcore",
  home: "Orbcore home",
  navigation: "Store",
  mobileNavigation: "Store menu",
  menu: "Browse the shop",
  openMenu: "Open menu",
  pages: "Pages",
  search: "Search",
  cart: "items in bag, view bag",
  moreEditions: "More editions",
  shopAll: "Shop all",
} as const

const NAVIGATION = {
  categories: [
    {
      id: "prints",
      name: "Prints",
      href: "#",
      editions: {
        id: "editions",
        name: "Editions",
        href: "#",
        columns: [
          [
            { name: "Color fields", href: "#" },
            { name: "Line work", href: "#" },
            { name: "Mezzotints", href: "#" },
            { name: "Monotypes", href: "#" },
            { name: "Posters", href: "#" },
            { name: "Small formats", href: "#" },
          ],
          [
            { name: "Signed editions", href: "#" },
            { name: "Diptychs", href: "#" },
            { name: "Large formats", href: "#" },
            { name: "Sample packs", href: "#" },
            { name: "Browse all", href: "#" },
          ],
        ],
      },
      groups: [
        {
          id: "framing",
          name: "Framing",
          href: "#",
          links: [
            { name: "Frames", href: "#" },
            { name: "Mounts", href: "#" },
            { name: "Glass", href: "#" },
            { name: "Hanging kits", href: "#" },
            { name: "Portfolios", href: "#" },
            { name: "Browse all", href: "#" },
          ],
        },
        {
          id: "collections",
          name: "Collections",
          href: "#",
          links: [
            { name: "New arrivals", href: "#" },
            { name: "Sale", href: "#" },
            { name: "Core", href: "#" },
            { name: "Signed editions", href: "#" },
          ],
        },
      ],
    },
    {
      id: "originals",
      name: "Originals",
      href: "#",
      editions: {
        id: "works",
        name: "Works",
        href: "#",
        columns: [
          [
            { name: "Drawings", href: "#" },
            { name: "Paintings", href: "#" },
            { name: "Studies", href: "#" },
            { name: "Diptychs", href: "#" },
            { name: "Sketchbooks", href: "#" },
            { name: "Collages", href: "#" },
          ],
          [
            { name: "Commissions", href: "#" },
            { name: "Archive", href: "#" },
            { name: "Large formats", href: "#" },
            { name: "Browse all", href: "#" },
          ],
        ],
      },
      groups: [
        {
          id: "framing",
          name: "Framing",
          href: "#",
          links: [
            { name: "Frames", href: "#" },
            { name: "Mounts", href: "#" },
            { name: "Glass", href: "#" },
            { name: "Hanging kits", href: "#" },
            { name: "Browse all", href: "#" },
          ],
        },
        {
          id: "collections",
          name: "Collections",
          href: "#",
          links: [
            { name: "Just added", href: "#" },
            { name: "Clearance", href: "#" },
            { name: "Studio picks", href: "#" },
          ],
        },
      ],
    },
  ],
  pages: [
    { name: "Studio", href: "#" },
    { name: "Stockists", href: "#" },
  ],
} as const

type StoreCategory = (typeof NAVIGATION.categories)[number]

// The phone drawer lists each group's links in one column.
function groupsOf(category: StoreCategory) {
  return [
    {
      id: category.editions.id,
      name: category.editions.name,
      href: category.editions.href,
      links: category.editions.columns.flat(),
    },
    ...category.groups,
  ]
}

const LINK =
  "inline-flex min-h-6 items-center rounded-sm text-sm underline-offset-4 outline-none hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
const MUTED = cn(LINK, "text-muted-foreground hover:text-foreground")
const ROW = cn(LINK, "min-h-11 w-full text-base")

type Link = { name: string; href: string }

function LinkList({
  links,
  label,
}: {
  links: readonly Link[]
  label?: string
}) {
  return (
    <ul role="list" aria-label={label} className="grid content-start gap-2">
      {links.map((link) => (
        <li key={link.name}>
          <a href={link.href} className={MUTED}>
            {link.name}
          </a>
        </li>
      ))}
    </ul>
  )
}

/**
 * A one-row shop header with the logo, the store navigation and links for
 * search and the bag. On wide screens the navigation sits in the middle,
 * and each category opens a menu of text links: a two-column list under
 * one heading and two smaller groups beside it. When the header is
 * narrower than 64rem, a menu button opens a sheet from the left where
 * each category folds open in an accordion.
 *
 * The words live in `COPY` and the categories and links in `NAVIGATION`,
 * at the top of the file. The links point to `#`.
 */
export default function StoreHeader02({
  cartCount = 0,
}: {
  /** The number shown beside the bag icon. Defaults to `0`. */
  cartCount?: number
}) {
  return (
    <header className="@container/shop-nav relative w-full border-b border-border bg-background text-foreground">
      <div className="mx-auto flex min-h-16 w-full max-w-7xl flex-wrap items-center gap-x-4 px-6 lg:px-10">
        <div className="-ml-2 flex flex-1 items-center gap-x-1 @5xl/shop-nav:hidden">
          <Sheet>
            <SheetTrigger
              render={
                <Button
                  variant="ghost"
                  size="icon"
                  aria-label={COPY.openMenu}
                />
              }
            >
              <ListIcon weight="light" aria-hidden="true" className="size-5" />
            </SheetTrigger>
            <SheetContent side="left" className="overflow-y-auto">
              <SheetHeader>
                <SheetTitle>{COPY.menu}</SheetTitle>
              </SheetHeader>
              <nav
                aria-label={COPY.mobileNavigation}
                className="grid gap-4 pb-6"
              >
                <Accordion className="mx-6 w-auto">
                  {NAVIGATION.categories.map((category) => (
                    <AccordionItem key={category.id} value={category.id}>
                      <AccordionTrigger className="focus-visible:ring-3 focus-visible:ring-ring/30 focus-visible:ring-inset">
                        {category.name}
                      </AccordionTrigger>
                      <AccordionContent className="grid gap-6">
                        <a href={category.href} className={ROW}>
                          {COPY.shopAll} {category.name}
                        </a>
                        {groupsOf(category).map((group) => (
                          <div
                            key={group.id}
                            className="grid min-w-0 content-start gap-4"
                          >
                            <a
                              href={group.href}
                              className={cn(LINK, "font-medium")}
                            >
                              {group.name}
                            </a>
                            <LinkList links={group.links} />
                          </div>
                        ))}
                      </AccordionContent>
                    </AccordionItem>
                  ))}
                </Accordion>
                <ul
                  role="list"
                  aria-label={COPY.pages}
                  className="grid border-t border-border px-6 pt-4"
                >
                  {NAVIGATION.pages.map((page) => (
                    <li key={page.name}>
                      <a href={page.href} className={ROW}>
                        {page.name}
                      </a>
                    </li>
                  ))}
                </ul>
              </nav>
            </SheetContent>
          </Sheet>
        </div>

        <a
          href="#"
          aria-label={COPY.home}
          className="inline-flex w-fit min-w-0 items-center gap-2 rounded-sm text-xl font-semibold focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring @5xl/shop-nav:flex-1"
        >
          <CircleIcon
            weight="light"
            aria-hidden="true"
            className="size-5 shrink-0"
          />
          <span className="hidden @md/shop-nav:inline">{COPY.brand}</span>
        </a>

        <NavigationMenu
          aria-label={COPY.navigation}
          delay={250}
          closeDelay={150}
          className="hidden @5xl/shop-nav:flex"
        >
          <NavigationMenuList>
            {NAVIGATION.categories.map((category) => (
              <NavigationMenuItem key={category.id}>
                <NavigationMenuTrigger>{category.name}</NavigationMenuTrigger>
                <NavigationMenuContent>
                  <div className="grid w-4xl grid-cols-4 gap-x-8 gap-y-10 p-4">
                    <div className="col-span-2 grid min-w-0 content-start gap-4">
                      <a
                        href={category.editions.href}
                        className={cn(LINK, "font-medium")}
                      >
                        {category.editions.name}
                      </a>
                      <div className="grid grid-cols-2 gap-x-4">
                        <LinkList links={category.editions.columns[0]} />
                        <LinkList
                          links={category.editions.columns[1]}
                          label={COPY.moreEditions}
                        />
                      </div>
                    </div>
                    {category.groups.map((group) => (
                      <div
                        key={group.id}
                        className="grid min-w-0 content-start gap-4"
                      >
                        <a
                          href={group.href}
                          className={cn(LINK, "font-medium")}
                        >
                          {group.name}
                        </a>
                        <LinkList links={group.links} />
                      </div>
                    ))}
                  </div>
                </NavigationMenuContent>
              </NavigationMenuItem>
            ))}
            {NAVIGATION.pages.map((page) => (
              <NavigationMenuItem key={page.name}>
                <NavigationMenuLink
                  href={page.href}
                  className={navigationMenuTriggerStyle()}
                >
                  {page.name}
                </NavigationMenuLink>
              </NavigationMenuItem>
            ))}
          </NavigationMenuList>
        </NavigationMenu>

        <div className="flex flex-1 items-center justify-end gap-x-1">
          <a
            href="#"
            aria-label={COPY.search}
            className={buttonVariants({ variant: "ghost", size: "icon" })}
          >
            <MagnifyingGlassIcon
              weight="light"
              aria-hidden="true"
              className="size-5"
            />
          </a>
          <a
            href="#"
            className={cn(buttonVariants({ variant: "ghost" }), "gap-1.5 px-2")}
          >
            <BasketIcon weight="light" aria-hidden="true" className="size-5" />
            <span className="text-sm tabular-nums">{cartCount}</span>
            <span className="sr-only">{COPY.cart}</span>
          </a>
        </div>
      </div>
    </header>
  )
}

Installation

pnpm dlx shadcn add @01orbital/store-header-02
npx shadcn add @01orbital/store-header-02
yarn shadcn add @01orbital/store-header-02
bunx --bun shadcn add @01orbital/store-header-02

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/blocks/store/store-header-02/index.tsx, as source you own and edit.

Usage

import StoreHeader02 from "@/components/blocks/store/store-header-02"

export default function Example() {
  return <StoreHeader02 />
}

API reference

Prop

Type

Dependencies

npm: @phosphor-icons/react, cn. The CLI installs them for you.

Registry pieces it installs with it:

  • accordion
  • button
  • navigation-menu
  • sheet