1 GitHub star = 1 new component.

Star the repo
Page Sections collectionsgridecommercecategories

Shop Collection Mosaic

Asymmetric collection grid with a tall feature tile, numbered tiles, item counts and underline links.

Live Preview

Usage

Copy and paste this code into your Astro page:

astro
---
import { ShopCollectionMosaic } from 'astro-component-kit/sections';
---

<ShopCollectionMosaic title="Shop by *intention*" items={[
  { title: 'The wardrobe', href: '/shop/?category=clothing', count: 24, icon: 'jacket', hue: 22 },
  { title: 'At the table', href: '/shop/?category=objects', count: 12, icon: 'mug', hue: 14 },
  { title: 'To carry', href: '/shop/?category=bags', count: 8, icon: 'bag', hue: 34 },
]} />
--- import { ShopCollectionMosaic } from 'astro-component-kit/sections'; --- <ShopCollectionMosaic title="Shop by *intention*" items={[ { title: 'The wardrobe', href: '/shop/?category=clothing', count: 24, icon: 'jacket', hue: 22 }, { title: 'At the table', href: '/shop/?category=objects', count: 12, icon: 'mug', hue: 14 }, { title: 'To carry', href: '/shop/?category=bags', count: 8, icon: 'bag', hue: 34 }, ]} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/lib/ShopCollectionMosaic.astro:

astro
---
/**
 * ShopCollectionMosaic — Asymmetric collection grid: one tall feature tile and smaller tiles,
 * each with an index number, title, item count and an underline link.
 *
 * @param {string} eyebrow - Small label. Default 'Collections'.
 * @param {string} title - Section heading. Default 'Shop by *intention*'.
 * @param {Array<{title: string, text?: string, href: string, count?: number, icon?: string, hue?: number, image?: string}>} items - 2–5 tiles; the first one is featured.
 * @param {string} linkLabel - Tile link text. Default 'Explore'.
 * @param {string} countLabel - Suffix after counts. Default 'pieces'.
 * @param {string} variant - Layout (feature-left|feature-right). Default 'feature-left'.
 * @param {string} size - Tile height (sm|md|lg). Default 'md'.
 * @param {string} className - Extra class.
 *
 * Accessibility: each tile is one link with a heading inside; art is decorative.
 */
import ProductMedia from '../ecommerce/ProductMedia.astro';

interface Tile { title: string; text?: string; href: string; count?: number; icon?: string; hue?: number; image?: string }
interface Props {
  eyebrow?: string;
  title?: string;
  items?: Tile[];
  linkLabel?: string;
  countLabel?: string;
  variant?: 'feature-left' | 'feature-right';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  eyebrow = 'Collections',
  title = 'Shop by *intention*',
  items = [
    { title: 'The wardrobe', text: 'Overshirts, knitwear and coats in natural fibres.', href: '#', count: 24, icon: 'jacket', hue: 22 },
    { title: 'At the table', text: 'Stoneware made by hand.', href: '#', count: 12, icon: 'mug', hue: 14 },
    { title: 'Light & air', text: 'Lamps and living things.', href: '#', count: 9, icon: 'plant', hue: 110 },
    { title: 'To carry', text: 'Bags that soften with age.', href: '#', count: 8, icon: 'bag', hue: 34 },
  ],
  linkLabel = 'Explore',
  countLabel = 'pieces',
  variant = 'feature-left',
  size = 'md',
  className = '',
} = Astro.props;

const parts = title.split(/\*(.+?)\*/g);
---

<section class:list={['scm', `scm--${variant}`, `scm--${size}`, className]}>
  <header class="scm__head">
    {eyebrow && <p class="scm__eyebrow">{eyebrow}</p>}
    <h2 class="scm__title">{parts.map((p, i) => (i % 2 ? <em>{p}</em> : p))}</h2>
  </header>
  <ul class="scm__grid">
    {items.slice(0, 5).map((t, i) => (
      <li class:list={['scm__item', { 'scm__item--feature': i === 0 }]}>
        <a class="scm__tile" href={t.href}>
          <span class="scm__art"><ProductMedia image={t.image} alt="" icon={t.icon} hue={t.hue} ratio="auto" variant="soft" size={i === 0 ? 'lg' : 'md'} /></span>
          <span class="scm__num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
          <span class="scm__body">
            <h3 class="scm__name">{t.title}</h3>
            {t.text && <span class="scm__text">{t.text}</span>}
            <span class="scm__link">{t.count !== undefined ? `${linkLabel} · ${t.count} ${countLabel}` : linkLabel}</span>
          </span>
        </a>
      </li>
    ))}
  </ul>
</section>

<style>
  .scm { container-type: inline-size; --scm-h: 300px; color: var(--c-text-1, #fff); }
  .scm--sm { --scm-h: 230px; }
  .scm--lg { --scm-h: 380px; }
  .scm__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-4, 1rem); margin-bottom: var(--sp-6, 1.5rem); padding-bottom: var(--sp-3, 0.75rem); border-bottom: 1px solid currentColor; }
  .scm__eyebrow { order: 2; margin: 0; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
  .scm__title { margin: 0; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: clamp(2rem, 4.5vw, 3.4rem); font-weight: 400; letter-spacing: -0.035em; line-height: 1; }
  .scm__title em { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }

  .scm__grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-auto-rows: var(--scm-h);
    gap: var(--sp-3, 0.75rem);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .scm__item--feature { grid-column: span 2; grid-row: span 2; }
  .scm--feature-right .scm__item--feature { grid-column: 3 / span 2; grid-row: 1 / span 2; }
  .scm__item:nth-child(4):last-child { grid-column: span 2; }

  .scm__tile {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    height: 100%;
    padding: var(--sp-5, 1.25rem);
    color: inherit;
    text-decoration: none;
    overflow: hidden;
    isolation: isolate;
    border-radius: var(--r-sm, 2px);
  }
  .scm__art { position: absolute; inset: 0; z-index: -1; }
  .scm__art :global(.p-media) { height: 100%; aspect-ratio: auto; transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1); }
  .scm__art :global(.p-media__art) { width: auto; height: 42%; margin-bottom: 22%; }
  .scm__tile:hover .scm__art :global(.p-media) { transform: scale(1.04); }
  .scm__tile::after { content: ''; position: absolute; inset: 35% 0 0; z-index: -1; background: linear-gradient(transparent, rgb(var(--paper-rgb, 5 5 5) / 0.9)); }
  .scm__num { position: absolute; top: var(--sp-4, 1rem); left: var(--sp-5, 1.25rem); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.1em; font-variant-numeric: tabular-nums; }
  .scm__body { display: grid; gap: 4px; }
  .scm__name { margin: 0; font-family: var(--font-display, Georgia, serif); font-size: 1.6rem; font-weight: 400; letter-spacing: -0.02em; line-height: 1.05; }
  .scm__item--feature .scm__name { font-size: clamp(2rem, 3.5vw, 3rem); }
  .scm__text { max-width: 24rem; font-size: 0.88rem; color: var(--c-text-2, #a3a3a3); }
  .scm__link { width: fit-content; margin-top: 6px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; border-bottom: 1px solid currentColor; padding-bottom: 2px; }
  .scm__tile:hover .scm__link { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
  .scm__tile:focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 3px; }

  @container (max-width: 900px) {
    .scm__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 220px; }
    .scm__item--feature, .scm--feature-right .scm__item--feature { grid-column: 1 / -1; grid-row: span 2; }
    .scm__item:nth-child(4):last-child { grid-column: auto; }
  }
  @container (max-width: 480px) {
    .scm__grid { grid-template-columns: 1fr; }
    .scm__item--feature, .scm--feature-right .scm__item--feature { grid-row: span 1; }
  }
  @media (prefers-reduced-motion: reduce) { .scm__art :global(.p-media) { transition: none; } }
</style>
--- /** * ShopCollectionMosaic — Asymmetric collection grid: one tall feature tile and smaller tiles, * each with an index number, title, item count and an underline link. * * @param {string} eyebrow - Small label. Default 'Collections'. * @param {string} title - Section heading. Default 'Shop by *intention*'. * @param {Array<{title: string, text?: string, href: string, count?: number, icon?: string, hue?: number, image?: string}>} items - 2–5 tiles; the first one is featured. * @param {string} linkLabel - Tile link text. Default 'Explore'. * @param {string} countLabel - Suffix after counts. Default 'pieces'. * @param {string} variant - Layout (feature-left|feature-right). Default 'feature-left'. * @param {string} size - Tile height (sm|md|lg). Default 'md'. * @param {string} className - Extra class. * * Accessibility: each tile is one link with a heading inside; art is decorative. */ import ProductMedia from '../ecommerce/ProductMedia.astro'; interface Tile { title: string; text?: string; href: string; count?: number; icon?: string; hue?: number; image?: string } interface Props { eyebrow?: string; title?: string; items?: Tile[]; linkLabel?: string; countLabel?: string; variant?: 'feature-left' | 'feature-right'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { eyebrow = 'Collections', title = 'Shop by *intention*', items = [ { title: 'The wardrobe', text: 'Overshirts, knitwear and coats in natural fibres.', href: '#', count: 24, icon: 'jacket', hue: 22 }, { title: 'At the table', text: 'Stoneware made by hand.', href: '#', count: 12, icon: 'mug', hue: 14 }, { title: 'Light & air', text: 'Lamps and living things.', href: '#', count: 9, icon: 'plant', hue: 110 }, { title: 'To carry', text: 'Bags that soften with age.', href: '#', count: 8, icon: 'bag', hue: 34 }, ], linkLabel = 'Explore', countLabel = 'pieces', variant = 'feature-left', size = 'md', className = '', } = Astro.props; const parts = title.split(/\*(.+?)\*/g); --- <section class:list={['scm', `scm--${variant}`, `scm--${size}`, className]}> <header class="scm__head"> {eyebrow && <p class="scm__eyebrow">{eyebrow}</p>} <h2 class="scm__title">{parts.map((p, i) => (i % 2 ? <em>{p}</em> : p))}</h2> </header> <ul class="scm__grid"> {items.slice(0, 5).map((t, i) => ( <li class:list={['scm__item', { 'scm__item--feature': i === 0 }]}> <a class="scm__tile" href={t.href}> <span class="scm__art"><ProductMedia image={t.image} alt="" icon={t.icon} hue={t.hue} ratio="auto" variant="soft" size={i === 0 ? 'lg' : 'md'} /></span> <span class="scm__num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span> <span class="scm__body"> <h3 class="scm__name">{t.title}</h3> {t.text && <span class="scm__text">{t.text}</span>} <span class="scm__link">{t.count !== undefined ? `${linkLabel} · ${t.count} ${countLabel}` : linkLabel}</span> </span> </a> </li> ))} </ul> </section> <style> .scm { container-type: inline-size; --scm-h: 300px; color: var(--c-text-1, #fff); } .scm--sm { --scm-h: 230px; } .scm--lg { --scm-h: 380px; } .scm__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-4, 1rem); margin-bottom: var(--sp-6, 1.5rem); padding-bottom: var(--sp-3, 0.75rem); border-bottom: 1px solid currentColor; } .scm__eyebrow { order: 2; margin: 0; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); } .scm__title { margin: 0; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: clamp(2rem, 4.5vw, 3.4rem); font-weight: 400; letter-spacing: -0.035em; line-height: 1; } .scm__title em { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); } .scm__grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: var(--scm-h); gap: var(--sp-3, 0.75rem); margin: 0; padding: 0; list-style: none; } .scm__item--feature { grid-column: span 2; grid-row: span 2; } .scm--feature-right .scm__item--feature { grid-column: 3 / span 2; grid-row: 1 / span 2; } .scm__item:nth-child(4):last-child { grid-column: span 2; } .scm__tile { position: relative; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; padding: var(--sp-5, 1.25rem); color: inherit; text-decoration: none; overflow: hidden; isolation: isolate; border-radius: var(--r-sm, 2px); } .scm__art { position: absolute; inset: 0; z-index: -1; } .scm__art :global(.p-media) { height: 100%; aspect-ratio: auto; transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1); } .scm__art :global(.p-media__art) { width: auto; height: 42%; margin-bottom: 22%; } .scm__tile:hover .scm__art :global(.p-media) { transform: scale(1.04); } .scm__tile::after { content: ''; position: absolute; inset: 35% 0 0; z-index: -1; background: linear-gradient(transparent, rgb(var(--paper-rgb, 5 5 5) / 0.9)); } .scm__num { position: absolute; top: var(--sp-4, 1rem); left: var(--sp-5, 1.25rem); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.1em; font-variant-numeric: tabular-nums; } .scm__body { display: grid; gap: 4px; } .scm__name { margin: 0; font-family: var(--font-display, Georgia, serif); font-size: 1.6rem; font-weight: 400; letter-spacing: -0.02em; line-height: 1.05; } .scm__item--feature .scm__name { font-size: clamp(2rem, 3.5vw, 3rem); } .scm__text { max-width: 24rem; font-size: 0.88rem; color: var(--c-text-2, #a3a3a3); } .scm__link { width: fit-content; margin-top: 6px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; border-bottom: 1px solid currentColor; padding-bottom: 2px; } .scm__tile:hover .scm__link { color: var(--shop-accent, var(--c-primary-light, #d4a017)); } .scm__tile:focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 3px; } @container (max-width: 900px) { .scm__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 220px; } .scm__item--feature, .scm--feature-right .scm__item--feature { grid-column: 1 / -1; grid-row: span 2; } .scm__item:nth-child(4):last-child { grid-column: auto; } } @container (max-width: 480px) { .scm__grid { grid-template-columns: 1fr; } .scm__item--feature, .scm--feature-right .scm__item--feature { grid-row: span 1; } } @media (prefers-reduced-motion: reduce) { .scm__art :global(.p-media) { transition: none; } } </style>

Quick Info

Category
Page Sections
Filename
ShopCollectionMosaic.astro
Dependencies
None — pure Astro + CSS
Tags
collectionsgridecommercecategories