Skip to content
Brok UI

Loading…

No results

Hero Categories

Open source

A shop-by-category storefront hero: a centered headline, subcopy, and primary call-to-action above a responsive grid of category cards. Each card is a real keyboard-accessible link with a muted image placeholder, category name, item-count badge, and a 'Shop {category}' arrow that flips under RTL. An optional feature tile spans two columns for editorial rhythm. Composes the card, button, and badge primitives with semantic tokens only.

Responsive Dark mode RTL Localized SEO

Preview

resources/views/blocks/hero-categories.blade.php Blade
@props([
    'heading'     => 'Shop by category',
    'subheading'  => 'Find your next favourite faster — start from a collection, not a search bar.',
    'primaryCta'  => 'Browse everything',
    'primaryHref' => '#',
    'categories'  => null,
])

@php
    $categories ??= [
        ['name' => 'New arrivals', 'count' => 128, 'href' => '#', 'feature' => true],
        ['name' => 'Footwear',     'count' => 112, 'href' => '#'],
        ['name' => 'Outerwear',    'count' => 84,  'href' => '#'],
        ['name' => 'Accessories',  'count' => 203, 'href' => '#'],
        ['name' => 'Knitwear',     'count' => 56,  'href' => '#'],
    ];
@endphp

{{--
    hero-categories — Shop-by-category storefront hero (Marketing, customer).
    TOP: headline, subcopy, optional primary CTA.
    GRID: category cards (real <x-ui.card> links) — bg-muted image area, name,
          item-count <x-ui.badge>, "Shop {name} →" with an RTL-flipped arrow.
          The first entry may be a feature tile spanning 2 columns.
    Distinct from store-hero (single-product editorial hero): the hook here is
    category NAVIGATION, a curated handful of entries — not one product, not a full catalogue.
    Semantic tokens only, RTL-safe logical props, WCAG AA.
--}}
<x-ui.section data-slot="hero-22" space="none" {{ $attributes }}>
    <div class="mx-auto max-w-6xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">

        {{-- ── Heading row ── --}}
        <div class="mx-auto mb-10 max-w-2xl text-center sm:mb-14">
            <h1 class="text-balance text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
                {{ __($heading) }}
            </h1>
            @if ($subheading)
                <p class="mx-auto mt-6 max-w-xl text-pretty text-lg text-muted-foreground">
                    {{ __($subheading) }}
                </p>
            @endif
            @if ($primaryCta)
                <div class="mt-8 flex justify-center">
                    <x-ui.button size="lg" :withArrow="true" href="{{ $primaryHref }}">
                        {{ __($primaryCta) }}
                    </x-ui.button>
                </div>
            @endif
        </div>

        {{-- ── Category cards ── --}}
        <ul role="list" class="grid grid-cols-2 gap-4 sm:gap-6 lg:grid-cols-3">
            @foreach ($categories as $category)
                @php $isFeature = ! empty($category['feature']); @endphp
                <li @class(['sm:col-span-2 lg:col-span-2' => $isFeature])>
                    <x-ui.card class="group relative h-full overflow-hidden p-0 transition-shadow hover:shadow-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">

                        {{-- Image placeholder area --}}
                        <div
                            @class([
                                'relative w-full overflow-hidden bg-muted',
                                'aspect-[2/1]' => $isFeature,
                                'aspect-[4/3]' => ! $isFeature,
                            ])
                            aria-hidden="true"
                        >
                            <svg
                                class="absolute inset-0 size-full text-muted-foreground/15 transition-transform duration-500 ease-out group-hover:scale-105"
                                viewBox="0 0 400 300"
                                fill="none"
                                stroke="currentColor"
                                stroke-width="1"
                                preserveAspectRatio="xMidYMid slice"
                            >
                                <circle cx="200" cy="150" r="60" stroke-width="0.8" />
                                <circle cx="200" cy="150" r="100" stroke-width="0.5" />
                                <line x1="-40" y1="0" x2="440" y2="320" />
                                <line x1="-40" y1="120" x2="440" y2="440" />
                            </svg>
                        </div>

                        {{-- Card body --}}
                        <div class="flex items-end justify-between gap-4 p-4 sm:p-6">
                            <div class="min-w-0">
                                <h2 @class([
                                    'truncate font-semibold text-foreground',
                                    'text-lg',
                                ])>
                                    {{-- Stretched link makes the whole card focusable & clickable --}}
                                    <a
                                        href="{{ $category['href'] ?? '#' }}"
                                        class="after:absolute after:inset-0 focus-visible:outline-none"
                                        aria-label="{{ __('Shop :category', ['category' => $category['name']]) }}"
                                    >
                                        {{ __($category['name']) }}
                                    </a>
                                </h2>
                                <p class="relative z-10 mt-2 inline-flex items-center gap-2 text-sm font-medium text-primary">
                                    {{ __('Shop :category', ['category' => $category['name']]) }}
                                    <svg
                                        class="size-4 shrink-0 transition-transform group-hover:translate-x-0.5 rtl:-scale-x-100"
                                        viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                                        stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"
                                    >
                                        <path d="M5 12h14M13 6l6 6-6 6" />
                                    </svg>
                                </p>
                            </div>

                            <x-ui.badge variant="secondary" class="relative z-10 shrink-0">
                                {{ trans_choice(':count item|:count items', $category['count'], ['count' => $category['count']]) }}
                            </x-ui.badge>
                        </div>
                    </x-ui.card>
                </li>
            @endforeach
        </ul>

    </div>
</x-ui.section>

Installation

terminal
php artisan ui:add blocks/hero-categories

Installs these dependencies automatically:

Registry contract

Install confidence

php artisan ui:add blocks/hero-categories writes only the generated targets below. The CLI validates each file hash before writing and prompts before replacing local changes unless --force is used.

Version
1.0.6
License
open
Stability
stable
Contract
v1
Foundation
≥ 1.0.0
Type Generated target
blade resources/views/blocks/hero-categories.blade.php

Registry dependencies

badge button card section

Package dependencies

composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.

hero-categories.md
# Brok UI block: Hero Categories (`hero-categories`)

A shop-by-category storefront hero: a centered headline, subcopy, and primary call-to-action above a responsive grid of category cards. Each card is a real keyboard-accessible link with a muted image placeholder, category name, item-count badge, and a 'Shop {category}' arrow that flips under RTL. An optional feature tile spans two columns for editorial rhythm. Composes the card, button, and badge primitives with semantic tokens only.

Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.

## Install

```bash
php artisan ui:add blocks/hero-categories
```

## Render it

```blade
<x-blocks.hero-categories />
```

## Props

- `heading` (string, default `Shop by category`) — Primary headline.
- `subheading` (string, default `Find your next favourite faster — start from a collection, not a search bar.`) — Supporting copy under the heading; falsy hides it.
- `primary-cta` (string, default `Browse everything`) — Primary button label; falsy hides the primary button.
- `primary-href` (string, default `#`) — Primary button destination.
- `categories` (mixed|null, default `null`) — Rows of {name, count, href, feature}; feature true spans that card across two columns. Null falls back to a default 5-item sample.

## Use when

- Opening a storefront landing page whose hook is category navigation: a curated handful of shoppable collections in a card grid rather than a single product or a search box.
- Wanting the first entry to render as a wide feature tile spanning two columns for editorial rhythm.

## Avoid when

- Showcasing one product or a single editorial hook rather than several categories; use store-hero (or store-hero-editorial/store-hero-video) for a single-product editorial hero instead.

## Rules

- Render the installed block with `<x-blocks.hero-categories />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.

## Links

- Docs: https://brokui.dev/blocks/hero-categories
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/hero-categories.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Guidance

Use when

  • Opening a storefront landing page whose hook is category navigation: a curated handful of shoppable collections in a card grid rather than a single product or a search box.
  • Wanting the first entry to render as a wide feature tile spanning two columns for editorial rhythm.

Avoid when

  • Showcasing one product or a single editorial hook rather than several categories; use store-hero (or store-hero-editorial/store-hero-video) for a single-product editorial hero instead.

Anti-patterns

  • Do not replace semantic props with conflicting utility classes.
  • Do not remove labels, focus styles, or state attributes.
Anatomy
heading subheading primary cta category card grid feature tile category image placeholder category name item-count badge shop link

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.hero-categories />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

This block composes installed <brok:*> primitives and semantic design tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by editing CSS variables; flip the preview to RTL to confirm it mirrors.

Source

The exact, editable file ui:add writes into your app. The preview above renders this same source — there are no preview-only blocks.

resources/views/blocks/hero-categories.blade.php Blade
@props([
    'heading'     => 'Shop by category',
    'subheading'  => 'Find your next favourite faster — start from a collection, not a search bar.',
    'primaryCta'  => 'Browse everything',
    'primaryHref' => '#',
    'categories'  => null,
])

@php
    $categories ??= [
        ['name' => 'New arrivals', 'count' => 128, 'href' => '#', 'feature' => true],
        ['name' => 'Footwear',     'count' => 112, 'href' => '#'],
        ['name' => 'Outerwear',    'count' => 84,  'href' => '#'],
        ['name' => 'Accessories',  'count' => 203, 'href' => '#'],
        ['name' => 'Knitwear',     'count' => 56,  'href' => '#'],
    ];
@endphp

{{--
    hero-categories — Shop-by-category storefront hero (Marketing, customer).
    TOP: headline, subcopy, optional primary CTA.
    GRID: category cards (real <x-ui.card> links) — bg-muted image area, name,
          item-count <x-ui.badge>, "Shop {name} →" with an RTL-flipped arrow.
          The first entry may be a feature tile spanning 2 columns.
    Distinct from store-hero (single-product editorial hero): the hook here is
    category NAVIGATION, a curated handful of entries — not one product, not a full catalogue.
    Semantic tokens only, RTL-safe logical props, WCAG AA.
--}}
<x-ui.section data-slot="hero-22" space="none" {{ $attributes }}>
    <div class="mx-auto max-w-6xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">

        {{-- ── Heading row ── --}}
        <div class="mx-auto mb-10 max-w-2xl text-center sm:mb-14">
            <h1 class="text-balance text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
                {{ __($heading) }}
            </h1>
            @if ($subheading)
                <p class="mx-auto mt-6 max-w-xl text-pretty text-lg text-muted-foreground">
                    {{ __($subheading) }}
                </p>
            @endif
            @if ($primaryCta)
                <div class="mt-8 flex justify-center">
                    <x-ui.button size="lg" :withArrow="true" href="{{ $primaryHref }}">
                        {{ __($primaryCta) }}
                    </x-ui.button>
                </div>
            @endif
        </div>

        {{-- ── Category cards ── --}}
        <ul role="list" class="grid grid-cols-2 gap-4 sm:gap-6 lg:grid-cols-3">
            @foreach ($categories as $category)
                @php $isFeature = ! empty($category['feature']); @endphp
                <li @class(['sm:col-span-2 lg:col-span-2' => $isFeature])>
                    <x-ui.card class="group relative h-full overflow-hidden p-0 transition-shadow hover:shadow-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">

                        {{-- Image placeholder area --}}
                        <div
                            @class([
                                'relative w-full overflow-hidden bg-muted',
                                'aspect-[2/1]' => $isFeature,
                                'aspect-[4/3]' => ! $isFeature,
                            ])
                            aria-hidden="true"
                        >
                            <svg
                                class="absolute inset-0 size-full text-muted-foreground/15 transition-transform duration-500 ease-out group-hover:scale-105"
                                viewBox="0 0 400 300"
                                fill="none"
                                stroke="currentColor"
                                stroke-width="1"
                                preserveAspectRatio="xMidYMid slice"
                            >
                                <circle cx="200" cy="150" r="60" stroke-width="0.8" />
                                <circle cx="200" cy="150" r="100" stroke-width="0.5" />
                                <line x1="-40" y1="0" x2="440" y2="320" />
                                <line x1="-40" y1="120" x2="440" y2="440" />
                            </svg>
                        </div>

                        {{-- Card body --}}
                        <div class="flex items-end justify-between gap-4 p-4 sm:p-6">
                            <div class="min-w-0">
                                <h2 @class([
                                    'truncate font-semibold text-foreground',
                                    'text-lg',
                                ])>
                                    {{-- Stretched link makes the whole card focusable & clickable --}}
                                    <a
                                        href="{{ $category['href'] ?? '#' }}"
                                        class="after:absolute after:inset-0 focus-visible:outline-none"
                                        aria-label="{{ __('Shop :category', ['category' => $category['name']]) }}"
                                    >
                                        {{ __($category['name']) }}
                                    </a>
                                </h2>
                                <p class="relative z-10 mt-2 inline-flex items-center gap-2 text-sm font-medium text-primary">
                                    {{ __('Shop :category', ['category' => $category['name']]) }}
                                    <svg
                                        class="size-4 shrink-0 transition-transform group-hover:translate-x-0.5 rtl:-scale-x-100"
                                        viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                                        stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"
                                    >
                                        <path d="M5 12h14M13 6l6 6-6 6" />
                                    </svg>
                                </p>
                            </div>

                            <x-ui.badge variant="secondary" class="relative z-10 shrink-0">
                                {{ trans_choice(':count item|:count items', $category['count'], ['count' => $category['count']]) }}
                            </x-ui.badge>
                        </div>
                    </x-ui.card>
                </li>
            @endforeach
        </ul>

    </div>
</x-ui.section>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.6
Status
Stable
License
open
Deprecation
Not deprecated
Contract
v1
Foundation
≥ 1.0.0