---
name: header-shop
title: Header Shop
url: https://ui.frontic.com/components/templates/header-shop.md
tier: templates
install: npx @frontic/ui add header-shop
files:
  - components/templates/header-shop/HeaderShop.vue
  - components/templates/header-shop/index.ts
registryDependencies:
  - badge
  - button
  - navigation-shop
dependencies:
  - "@lucide/vue"
---

# Header Shop

Store header with an announcement bar, logo, a navigation slot, a search slot that sits inline on wide screens and behind a button that opens an overlay on phones, and account and cart actions.

## Install

```sh
npx @frontic/ui add header-shop
```

Built from: [Badge](https://ui.frontic.com/components/ui/badge.md), [Button](https://ui.frontic.com/components/ui/button.md), [Navigation Shop](https://ui.frontic.com/components/sections/navigation-shop.md)

Used by: [Category Grid](https://ui.frontic.com/components/templates/category-grid.md)

npm packages: `@lucide/vue`

## Examples

```vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { HeaderShop, type NavigationCategory, type NavigationLink } from '@/components/templates/header-shop'
import { SearchSuggestions, type SearchProduct, type SearchResult } from '@/components/sections/search-suggestions'

const image = (name: string) => `https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/${name}.jpg`

const categories: NavigationCategory[] = [
  {
    name: 'Women',
    featured: [
      { name: 'Winterwonder Parka', href: '#', imageSrc: image('winterwonder-parka'), imageAlt: 'Winterwonder Parka' },
      { name: 'Snowflake Ski Goggles', href: '#', imageSrc: image('snowflake-ski-goggles'), imageAlt: 'Snowflake Ski Goggles' },
    ],
    columns: [
      {
        name: 'Winter',
        links: [
          { name: 'Jackets', href: '#' },
          { name: 'Beanies', href: '#' },
          { name: 'Goggles', href: '#' },
          { name: 'Helmets', href: '#' },
          { name: 'Gloves', href: '#' },
        ],
      },
      {
        name: 'Equipment',
        links: [{ name: 'Backpacks', href: '#' }],
      },
    ],
  },
  {
    name: 'Men',
    featured: [
      { name: 'Frost Escape Parka', href: '#', imageSrc: image('frost-escape-parka'), imageAlt: 'Frost Escape Parka' },
      { name: 'TrailMaster 22', href: '#', imageSrc: image('trailmaster-22'), imageAlt: 'TrailMaster 22' },
    ],
    columns: [
      {
        name: 'Winter',
        links: [
          { name: 'Jackets', href: '#' },
          { name: 'Beanies', href: '#' },
          { name: 'Goggles', href: '#' },
          { name: 'Helmets', href: '#' },
        ],
      },
      {
        name: 'Equipment',
        links: [{ name: 'Backpacks', href: '#' }],
      },
    ],
  },
  { name: 'Equipment', href: '#' },
]

const pages: NavigationLink[] = [{ name: 'Sale', href: '#', highlight: true }]

// The Women flyout shows open where the bar has room for it; on a phone the bar shows closed.
const menu = ref('Women')
const product = (name: string, slug: string, meta: string, amount: number): SearchProduct => ({ name, href: `#/p/${slug}`, image: image(slug), meta, price: { amount, currency: 'EUR' } })
const catalog = [
  product('Frost Escape Parka', 'frost-escape-parka', 'Sun yellow · Jackets', 14900),
  product('Winterwonder Parka', 'winterwonder-parka', 'Jackets', 18900),
  product('Snowflake Ski Goggles', 'snowflake-ski-goggles', 'Mint · Goggles', 12700),
  product('TrailMaster 22', 'trailmaster-22', 'Brown · 20L · Backpacks', 14100),
]
const suggestions = [{ label: 'parka' }, { label: 'ski goggles' }, { label: 'backpack' }]
const query = ref('')
const results = computed<SearchResult>(() => {
  const term = query.value.trim().toLowerCase()
  const has = (text: string) => text.toLowerCase().includes(term)
  return { query: query.value.trim(), products: catalog.filter((item) => has(item.name) || has(item.meta ?? '')), categories: [], brands: [] }
})
</script>

<template>
  <div class="@container">
    <div class="@5xl:min-h-130">
      <HeaderShop v-model:menu="menu" announcement="Free shipping and returns on all orders" logo-text="Basecamp" :categories="categories" :pages="pages" :cart-count="3">
        <template #search="{ presentation }">
          <SearchSuggestions v-model:query="query" :presentation="presentation" :results="results" :suggestions="suggestions" placeholder="Search jackets, goggles, packs" />
        </template>
      </HeaderShop>
    </div>
  </div>
</template>
```

The example also uses `search-suggestions`, which `add header-shop` leaves out: `npx @frontic/ui add search-suggestions`.

## API reference

### HeaderShop

Shows an optional announcement, the logo, navigation, and search, account and cart buttons. Renders a `<header>`.

`data-slot="header-shop"` · `HeaderShop.vue`

Styling: [`logo`](https://ui.frontic.com/DESIGN.md#logo). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `categories` | `NavigationCategory[]` | `[]` | The top-level categories; one with featured tiles or link columns opens a flyout, one without is a plain link. |
| `pages` | `NavigationLink[]` | `[]` | Plain links after the categories, such as Sale or Journal. |
| `menuLabel` | `string` |  | Screen-reader name of the button that opens the mobile menu. |
| `sheetTitle` | `string` |  | Screen-reader title of the mobile menu. |
| `featuredCta` | `string` |  | Shown under each featured tile's name, e.g. `Shop now`. |
| `announcement` | `string` | `''` | Shown in a bar above the header; hidden when empty. |
| `logoText` | `string` | `'STORE'` | Shown as the logo, or as the logo image's alt text when `logoSrc` is set. |
| `logoSrc` | `string` | `''` | URL of the logo image; `logoText` shows in its place when empty. |
| `homeHref` | `string` | `'/'` | Where the logo links to. |
| `cartCount` | `number` | `0` | Number on the cart button's badge; the badge hides at 0. |
| `searchLabel` | `string` | `'Search'` | Screen-reader name of the search button. |
| `accountLabel` | `string` | `'Account'` | Screen-reader name of the account button. |
| `cartLabel` | `string` | `'Cart'` | Screen-reader name of the cart button. |
| `cartOverflowLabel` | `string` | `'9+'` | Shown on the cart badge in place of counts above 9. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `menu` | `string` | `''` | The navigation's open flyout, by category name. |
| `menuOpen` | `boolean` | `false` | Whether the navigation's mobile sheet is open. |

| Event | Payload | Description |
| --- | --- | --- |
| `search` |  | Fires when the shopper clicks the search button; not used when the search slot fills it. |
| `update:menu` | `value: string` | Fires with the new value when `menu` changes; `v-model:menu` binds it. |
| `update:menuOpen` | `value: boolean` | Fires with the new value when `menuOpen` changes; `v-model:menuOpen` binds it. |
| `account` |  | Fires when the shopper clicks the account button. |
| `cart` |  | Fires when the shopper clicks the cart button. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `navigation` | `{ categories: NavigationCategory[]; pages: NavigationLink[]; }` | Replaces the navigation; gets the categories and pages. |
| `search` | `{ presentation: "inline" \| "overlay"; }` | Holds the search, rendered twice: inline in the bar from |

## Exports

```ts
import { type NavigationCategory, type NavigationLink } from '@/components/templates/header-shop'
```

- `NavigationCategory`, `NavigationLink`: from [Navigation Shop](https://ui.frontic.com/components/sections/navigation-shop.md)

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`header-shop`](https://ui.frontic.com/DESIGN.md#header-shop) |
| `announcement` | [`header-shop-announcement`](https://ui.frontic.com/DESIGN.md#header-shop-announcement) |
| `bar` | [`header-shop-bar`](https://ui.frontic.com/DESIGN.md#header-shop-bar) |
| `logo` | [`logo`](https://ui.frontic.com/DESIGN.md#logo) |
| `focus-ring` | [`focus-ring`](https://ui.frontic.com/DESIGN.md#focus-ring) |

States with a look of their own: focus.

## Source

`components/templates/header-shop/HeaderShop.vue`

```vue
<script lang="ts">
/** Shows an optional announcement, the logo, navigation, and search, account and cart buttons. */
export default {}
</script>

<script setup lang="ts">
import { SearchIcon, UserIcon, ShoppingBagIcon } from '@lucide/vue'

import type { HTMLAttributes } from 'vue'
import { Badge } from '@library/components/ui/badge'
import { Button } from '@library/components/ui/button'
import { NavigationShop, type NavigationCategory, type NavigationLink } from '@library/components/sections/navigation-shop'

const props = withDefaults(
  defineProps<{
    /** The top-level categories; one with featured tiles or link columns opens a flyout, one without is a plain link. */
    categories?: NavigationCategory[]
    /** Plain links after the categories, such as Sale or Journal. */
    pages?: NavigationLink[]
    /** Screen-reader name of the button that opens the mobile menu. */
    menuLabel?: string
    /** Screen-reader title of the mobile menu. */
    sheetTitle?: string
    /** Shown under each featured tile's name, e.g. `Shop now`. */
    featuredCta?: string
    /** Shown in a bar above the header; hidden when empty. */
    announcement?: string
    /** Shown as the logo, or as the logo image's alt text when `logoSrc` is set. */
    logoText?: string
    /** URL of the logo image; `logoText` shows in its place when empty. */
    logoSrc?: string
    /** Where the logo links to. */
    homeHref?: string
    /** Number on the cart button's badge; the badge hides at 0. */
    cartCount?: number
    /** Screen-reader name of the search button. */
    searchLabel?: string
    /** Screen-reader name of the account button. */
    accountLabel?: string
    /** Screen-reader name of the cart button. */
    cartLabel?: string
    /** Shown on the cart badge in place of counts above 9. */
    cartOverflowLabel?: string
    class?: HTMLAttributes['class']
  }>(),
  {
    categories: () => [],
    pages: () => [],
    announcement: '',
    logoText: 'STORE',
    logoSrc: '',
    homeHref: '/',
    cartCount: 0,
    searchLabel: 'Search',
    accountLabel: 'Account',
    cartLabel: 'Cart',
    cartOverflowLabel: '9+',
  },
)
/** The navigation's open flyout, by category name. */
const menu = defineModel<string>('menu', { default: '' })
/** Whether the navigation's mobile sheet is open. */
const menuOpen = defineModel<boolean>('menuOpen', { default: false })
const emit = defineEmits<{
  /** Fires when the shopper clicks the search button; not used when the search slot fills it. */
  search: []
  /** Fires when the shopper clicks the account button. */
  account: []
  /** Fires when the shopper clicks the cart button. */
  cart: []
}>()
defineSlots<{
  /** Replaces the navigation; gets the categories and pages. */
  navigation?: (props: { categories: NavigationCategory[]; pages: NavigationLink[] }) => unknown
  /** Holds the search, rendered twice: inline in the bar from @5xl, and as the button that opens an overlay below that. */
  search?: (props: { presentation: 'inline' | 'overlay' }) => unknown
}>()
</script>

<template>
  <header data-slot="header-shop" :class="['fui-header-shop @container sticky top-0 z-50', props.class]">
    <div v-if="announcement" data-slot="header-shop-announcement" class="fui-header-shop-announcement">
      <div class="mx-auto flex h-8 w-full max-w-384 items-center justify-center px-4 @2xl:px-6 @5xl:px-8">{{ announcement }}</div>
    </div>
    <div data-slot="header-shop-bar" class="fui-header-shop-bar">
      <div class="mx-auto flex h-14 w-full max-w-384 items-center justify-between gap-6 px-4 @2xl:px-6 @5xl:px-8">
        <a :href="homeHref" data-slot="logo" class="fui-logo outline-none focus-visible:fui-focus-ring">
          <img v-if="logoSrc" :src="logoSrc" :alt="logoText" class="h-8 w-auto" />
          <template v-else>{{ logoText }}</template>
        </a>
        <div class="flex-1">
          <slot name="navigation" :categories="categories" :pages="pages">
            <NavigationShop v-model:menu="menu" v-model:open="menuOpen" :categories="categories" :pages="pages" :menu-label="menuLabel" :sheet-title="sheetTitle" :featured-cta="featuredCta" />
          </slot>
        </div>
        <div v-if="$slots.search" class="hidden w-64 @5xl:block">
          <slot name="search" presentation="inline" />
        </div>
        <div class="flex items-center gap-1">
          <div v-if="$slots.search" class="flex @5xl:hidden">
            <slot name="search" presentation="overlay" />
          </div>
          <Button v-else variant="ghost" color="neutral" size="icon-md" :aria-label="searchLabel" @click="emit('search')">
            <SearchIcon data-icon />
          </Button>
          <div class="hidden @2xl:flex">
            <Button variant="ghost" color="neutral" size="icon-md" :aria-label="accountLabel" @click="emit('account')">
              <UserIcon data-icon />
            </Button>
          </div>
          <Button variant="ghost" color="neutral" size="icon-md" :aria-label="cartLabel" class="relative" @click="emit('cart')">
            <ShoppingBagIcon data-icon />
            <Badge v-if="cartCount > 0" variant="primary" size="sm" class="absolute -top-1 -inset-e-1">{{ cartCount > 9 ? cartOverflowLabel : cartCount }}</Badge>
          </Button>
        </div>
      </div>
    </div>
  </header>
</template>
```

## API of the items the example uses

### Search Suggestions

[Full docs](https://ui.frontic.com/components/sections/search-suggestions.md) · `npx @frontic/ui add search-suggestions`

#### SearchSuggestions

A storefront search: products, categories and brands while the shopper types, suggestions and curated picks before. Renders a `<div>`.

`data-slot="search-suggestions"` · `SearchSuggestions.vue`

Styling: `search-suggestions-image`, which extends [`thumbnail`](https://ui.frontic.com/DESIGN.md#thumbnail); [`line-title`](https://ui.frontic.com/DESIGN.md#line-title); [`meta`](https://ui.frontic.com/DESIGN.md#meta). Change them there and every part that extends them follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `results` | `SearchResult \| null`: `null \| SearchResult` |  | The shop's answer to the query; shown only while it answers the query in the field. |
| `loading` | `boolean` | `false` | Shows the loading rows while the shop's search runs. |
| `suggestions` | `SearchSuggestion[]` | `[]` | Searches offered as tags before typing and when nothing is found. |
| `collections` | `SearchCollection[]` | `[]` | Curated rows of products, such as staff picks, shown before typing and when nothing is found. |
| `highlight` | `string \| string[]` |  | What counts as a match in the names: the typed query when left out, or the terms the shop's search matched. |
| `presentation` | `'inline' \| 'overlay'` | `'inline'` | Inline floats the panel under the field; overlay shows a search button that opens it full screen on phones and near the top on wide screens. |
| `searchHref` | `((query: string) => string)`: `(query: string): string` |  | The results page for a query; links the suggestion tags and the "See all" row. Without it, tags run in the panel. |
| `shortcut` | `boolean` | `false` | Lets "/" focus the field, or open the overlay, while focus is not in another field. |
| `placeholder` | `string` | `'Search'` | Shown in the empty field. |
| `searchLabel` | `string` | `'Search'` | Screen-reader name of the field and of the overlay's search button. |
| `productsLabel` | `string` | `'Products'` | The heading over the products. |
| `categoriesLabel` | `string` | `'Categories'` | The heading over the categories. |
| `brandsLabel` | `string` | `'Brands'` | The heading over the brands. |
| `popularLabel` | `string` | `'Popular searches'` | The heading over the suggested searches before typing. |
| `noResultsLabel` | `((query: string) => string)`: `(query: string): string` | ``No results for '${query}'. Try one of these:`` | Builds the line shown when a search finds nothing. |
| `seeAllLabel` | `((total: number) => string)`: `(total: number): string` | ``See all ${total} results`` | Builds the "See all" row from the total; it shows when the total is more than the products listed. |
| `resultsAnnouncement` | `((count: number) => string)`: `(count: number): string` | `count === 1 ? '1 result' : `${count} results`` | Builds what screen readers hear when results land. |
| `cancelLabel` | `string` | `'Cancel'` | The overlay's Cancel button on phones. |
| `locale` | `string` |  | The products' price locale, e.g. `de-DE`; the ConfigProvider's when left out. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `query` | `string` | `''` | What the shopper typed; run the shop's search on every change. |
| `open` | `boolean` | `false` | Whether the panel is open, or the overlay in that presentation. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:open` | `value: boolean` | Fires with the new value when `open` changes; `v-model:open` binds it. |
| `select` | `href: string` | The shopper opened a row; gets its `href`, which the link also follows. |
| `submit` | `query: string` | The shopper asked for every result for the query. |
| `update:query` | `value: string` | Fires with the new value when `query` changes; `v-model:query` binds it. |
