Card grid

Try it yourself

Catalog sizes control compact card geometry. Editorial cards use content spans and keep descriptions visible. The code below follows your selections.

Example.svelte
svelte
<script lang="ts">
  import { CardGridSection } from '@page-sections/svelte';
  const list = [
    {
      "id": "coast",
      "title": "Notes from the coast",
      "eyebrow": "Field notes",
      "description": "Make space for stories, observations and a slower way of seeing.",
      "href": "#playground",
      "image": {
        "src": "../../../coast.svg",
        "alt": "Sandstone cliffs beside the sea"
      }
    },
    {
      "id": "hills",
      "title": "A different perspective",
      "description": "Cards without an eyebrow still align with their neighbours.",
      "href": "#playground",
      "image": {
        "src": "../../../landscape.svg",
        "alt": "Layers of green hills"
      }
    },
    {
      "id": "journey",
      "title": "Small beginnings, longer journeys",
      "eyebrow": "Experiments",
      "description": "Try a different image ratio and notice that the text keeps its own space.",
      "href": "#playground",
      "image": {
        "src": "../../../coast.svg",
        "alt": "A sunlit coastline"
      }
    },
    {
      "id": "more",
      "title": "Room to grow",
      "eyebrow": "Ideas",
      "description": "Explore equal heights and responsive columns with content of different lengths.",
      "href": "#playground",
      "image": {
        "src": "../../../landscape.svg",
        "alt": "A landscape of hills"
      }
    }
  ];
</script>

<CardGridSection
  section={{ id: 'collection', title: 'Collection' }}
  {list}
  cardSize="m"
  cardTone="muted"
  cardVariant="catalog"
  mediaAspect="3/2"
  mediaFit="contain"
  textAlign="start"
/>

# CardGridSection

Use a catalog for compact collections and editorial cards for longer website content. Both use the same container-based ContentGrid.

svelte
<script lang="ts">
  import { CardGridSection, type CardItem } from '@page-sections/svelte';
  const list: CardItem[] = [
    {
      id: 'project',
      title: 'A first project',
      description: 'Room for a complete description.',
      footer: 'Design and development',
      actions: [{ label: 'Explore', href: '/projects/first', variant: 'primary' }]
    }
  ];
</script>

<CardGridSection
  section={{ id: 'projects', title: 'Projects', headline: 'Selected work' }}
  {list}
  cardVariant="editorial"
  itemSpan={{ xs: 4, s: 3, m: 4, l: 4 }}
/>

Props

Generic over T extends CardItem.

PropTypeDefault
sectionSectionDefinitionRequired
listT[]Required; unique itemKey ?? id per occurrence
cardSizes/m/lm; uniform catalog size
cardVariantcatalog/editorialcatalog
cardToneSurfaceTone: standard/mutedstandard
mediaAspectCardMediaAspect: 3/2, 4/3, 1/13/2; uniform media frame
mediaFitCardMediaFit: contain/covercontain for catalog, cover for editorial
itemSpanContentSpanxs=4, s=3, m=4, l=4; editorial only
textAlignstart/centerstart
media, surfaceSnippet<[T]>Optional replacements for card parts
itemContentSnippet<[T]>Extra editorial copy
cardSnippet<[T, { cardSize: CardSize; variant: CardVariant; tone: SurfaceTone; textAlign: 'start' | 'center'; mediaAspect: CardMediaAspect; mediaFit: CardMediaFit; reserveEyebrow: boolean }]>Optional complete card replacement
oncardready(parts: CardElements<T>) => void | (() => void)Attachment/cleanup hook for default Card

A complete card replacement owns its markup, accessibility and enhancement lifecycle. Media/surface snippets retain the default Card and its measured elements.

Layout and styling

Set cardTone="muted" to distinguish cards from a standard section background. JSON sections use cardTone: "muted". This chooses the theme surface without changing the section background, media layout or equal heights. Forward options.tone when providing a complete custom card snippet.

Catalog S/M/L spans 2/2/4 columns; L spans 3 on the S grid. Only the media frame has a fixed ratio, selected with mediaAspect. The text area is added below it: two title lines for S/M, three for L, plus 12px padding on each side. S hides eyebrows. If any M/L item has an eyebrow, all cards reserve a separate label line, including blank items. Labels never consume title lines. S uses 12px titles, M 14px and L 18px. Titles clamp visually while their full text remains in the link's accessible name and title attribute.

All grid rows share the tallest required card height, including rows with short titles or missing labels. Editorial cards retain natural copy, independent title/action links and bottom-aligned footers/actions. Descriptions, children and footers are editorial-only. A complete card replacement must fill its slot and use the supplied layout context to retain alignment.

Use mediaFit="contain" for complete objects with an 8px media inset, or mediaFit="cover" for edge-to-edge cropped photos. Both use the same selected mediaAspect. ImageData.aspect/objectFit do not override this collection-level contract. Compare 3:2 and 1:1 in the interactive example above.

Per-item size and the old auto-fit minimum widths were removed in 0.2. See shared layout reference.

Accessibility and SSR

A catalog card with href is one native link. Do not put links/buttons into its media/surface snippets. Editorial cards use article markup, with separate title and action links. Heading levels inherit from the surrounding section. All content and links render on the server; attachment callbacks run after hydration.

Related: Card and Image, CarouselSection, Section and grids.