---
title: "PostsList"
description: "The unified posts listing: grid, row and lead layouts, paging and injected items."
requested_language: ar
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/ar/components/posts-list
version: unreleased (main)
docs_index: https://docs.systhema.app/ar/llms.txt
---
> This page isn't translated yet. Showing English.


`PostsList` renders a list of posts as a grid or a row list, with an optional full-bleed highlight card, a lead grid, paging and injected items such as ads. Latest Posts, archive listings, related posts and the Payload `posts` block all render through it. It takes plain [`PostView`](https://docs.systhema.app/ar/components/posts-helpers.md#postview) objects, so it works with any data source, not only Payload.

```tsx preview iframe height=900 title="Grid with a highlighted first post"
import { PostsList, type PostView } from '@systhemaui/next'

function post(id: number, title: string, label: string, image: string): PostView {
  return {
    id,
    title,
    label,
    href: '#',
    excerpt: 'How a small team keeps a large site consistent without slowing its editors down.',
    tags: [],
    postType: 'article',
    author: { name: 'Mira Kovač' },
    publishedAt: `2026-09-${10 + id}T09:00:00.000Z`,
    media: { url: `/demo-assets/${image}`, alt: '', width: 1800, height: 1200 },
    doc: null,
  }
}

const posts = [
  post(4, 'Designing with tokens from day one', 'Design', 'city.webp'),
  post(3, 'A calmer content model for editors', 'Editorial', 'workspace.webp'),
  post(2, 'Shipping a multilingual site in a week', 'Engineering', 'mountains.webp'),
  post(1, 'A field guide to spacing', 'Design', 'coast.webp'),
]

export default function Demo() {
  return <PostsList items={posts} view="grid3" highlightFirst />
}
```

## Import

```tsx
import { PostsList } from '@systhemaui/next'
```

In a React app without Next.js, import it from `@systhemaui/react`. The Next.js version renders media with `next/image` and links with `next/link`; see [Next.js](#nextjs).

## Layouts

`view` sets the layout of the list: `grid2`, `grid3`, `grid4` (the default) or `row`. Every grid is one column below `md`. `grid2` turns two-up from `md`; `grid3` and `grid4` are two-up from `md` and three-up or four-up from `lg`. Each card gets the matching `layout`, which sets its media aspect ratio and which parts it shows (see [PostCard](https://docs.systhema.app/ar/components/post-card.md#default-parts-per-layout)).

### Grid

```tsx preview iframe height=560 title="grid3"
import { PostsList, type PostView } from '@systhemaui/next'

function post(id: number, title: string, label: string, image: string): PostView {
  return {
    id,
    title,
    label,
    href: '#',
    excerpt: 'How a small team keeps a large site consistent without slowing its editors down.',
    tags: [],
    postType: 'article',
    author: { name: 'Mira Kovač' },
    publishedAt: `2026-09-${10 + id}T09:00:00.000Z`,
    media: { url: `/demo-assets/${image}`, alt: '', width: 1800, height: 1200 },
    doc: null,
  }
}

const posts = [
  post(3, 'A calmer content model for editors', 'Editorial', 'workspace.webp'),
  post(2, 'Shipping a multilingual site in a week', 'Engineering', 'city.webp'),
  post(1, 'A field guide to spacing', 'Design', 'coast.webp'),
]

export default function Demo() {
  return <PostsList items={posts} view="grid3" />
}
```

### Row list with dividers

`row` puts the media beside the text from `md` up and stacks it below. Add `dividers` to draw a 1px `--color-separator-line` hairline between rows. Grids ignore `dividers`.

```tsx preview iframe height=640 title="row with dividers"
import { PostsList, type PostView } from '@systhemaui/next'

function post(id: number, title: string, label: string, image: string): PostView {
  return {
    id,
    title,
    label,
    href: '#',
    tags: [],
    postType: 'article',
    author: { name: 'Mira Kovač' },
    publishedAt: `2026-09-${10 + id}T09:00:00.000Z`,
    media: { url: `/demo-assets/${image}`, alt: '', width: 1800, height: 1200 },
    doc: null,
  }
}

const posts = [
  post(3, 'A calmer content model for editors', 'Editorial', 'workspace.webp'),
  post(2, 'Shipping a multilingual site in a week', 'Engineering', 'city.webp'),
  post(1, 'A field guide to spacing', 'Design', 'coast.webp'),
]

export default function Demo() {
  return <PostsList items={posts} view="row" dividers />
}
```

### Lead grid and tail list

`leadCount` renders the first N cards in a separate lead grid, laid out by `leadView` (default `grid3`), and the rest below it in `view`. With `highlightFirst`, the count starts after the highlight card. A `leadCount` of `0`, or one at least as large as the number of cards, renders a single uniform list.

```tsx preview iframe height=1100 title="Highlight, a two-card lead grid, then a row list"
import { PostsList, type PostView } from '@systhemaui/next'

function post(id: number, title: string, label: string, image: string): PostView {
  return {
    id,
    title,
    label,
    href: '#',
    excerpt: 'How a small team keeps a large site consistent without slowing its editors down.',
    tags: [],
    postType: 'article',
    author: { name: 'Mira Kovač' },
    publishedAt: `2026-09-${10 + id}T09:00:00.000Z`,
    media: { url: `/demo-assets/${image}`, alt: '', width: 1800, height: 1200 },
    doc: null,
  }
}

const posts = [
  post(5, 'Designing with tokens from day one', 'Design', 'city.webp'),
  post(4, 'A calmer content model for editors', 'Editorial', 'workspace.webp'),
  post(3, 'Shipping a multilingual site in a week', 'Engineering', 'mountains.webp'),
  post(2, 'A field guide to spacing', 'Design', 'coast.webp'),
  post(1, 'Choosing a type pairing', 'Design', 'abstract-square.webp'),
]

export default function Demo() {
  return (
    <PostsList items={posts} highlightFirst leadCount={2} leadView="grid2" view="row" dividers />
  )
}
```

## Examples

### Gaps

Grids default to a `sm` column gap and a `md` row gap, and the row list to a `md` row gap. `gapX` and `gapY` override one axis each with a gap token (`none`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`), applied inline as `column-gap: var(--gap-<size>)` and `row-gap: var(--gap-<size>)`.

```tsx
<PostsList items={posts} view="grid4" gapX="md" gapY="lg" />
```

### Card options

`cardConfig` is passed to every default card: `show` turns parts on or off, `classNames` overrides a part's classes, and `aspectRatio`, `mediaWidth`, `bodyAlign` and `mediaGap` set inline presentation values. `highlightConfig` does the same for the highlight card, and `leadCardConfig` for the lead grid (it falls back to `cardConfig`). The parts are listed on [PostCard](https://docs.systhema.app/ar/components/post-card.md#parts).

```tsx preview iframe height=560 title="Square media, no excerpt, a read-more link"
import { PostsList, type PostView } from '@systhemaui/next'

function post(id: number, title: string, label: string, image: string): PostView {
  return {
    id,
    title,
    label,
    href: '#',
    excerpt: 'How a small team keeps a large site consistent without slowing its editors down.',
    tags: [],
    postType: 'article',
    author: { name: 'Mira Kovač' },
    publishedAt: `2026-09-${10 + id}T09:00:00.000Z`,
    media: { url: `/demo-assets/${image}`, alt: '', width: 1800, height: 1200 },
    doc: null,
  }
}

const posts = [
  post(3, 'A calmer content model for editors', 'Editorial', 'workspace.webp'),
  post(2, 'Shipping a multilingual site in a week', 'Engineering', 'city.webp'),
  post(1, 'A field guide to spacing', 'Design', 'coast.webp'),
]

export default function Demo() {
  return (
    <PostsList
      items={posts}
      view="grid3"
      cardConfig={{ show: { excerpt: false, readMore: true }, aspectRatio: '1/1' }}
    />
  )
}
```

### Loading more posts

`items` is the first page. To load more, set `loading` and pass `fetchMore(page)`, which returns the next page's `PostView[]`, together with `paging` (`{ total, page, limit }`) so the list knows when it has everything:

| `loading`    | Behaviour                                                                                      |
| ------------ | ---------------------------------------------------------------------------------------------- |
| `none`       | Renders `items` only (default).                                                                |
| `loadMore`   | A **Load more** button appends the next page.                                                  |
| `infinite`   | The next page is appended when a sentinel below the list scrolls within 200px of the viewport. |
| `pagination` | Previous and next buttons with a `page / total` counter replace the rendered page.             |

`PostsList` never fetches on its own, so `fetchMore` is where you call your API. A function prop makes the calling module a client module.

```tsx preview iframe height=1000 title="loading="loadMore""
'use client'

import { PostsList, type PostView } from '@systhemaui/next'

function post(id: number, title: string, image: string): PostView {
  return {
    id,
    title,
    label: 'Journal',
    href: '#',
    tags: [],
    postType: 'article',
    author: { name: 'Mira Kovač' },
    publishedAt: `2026-09-${10 + id}T09:00:00.000Z`,
    media: { url: `/demo-assets/${image}`, alt: '', width: 1800, height: 1200 },
    doc: null,
  }
}

const all = [
  post(6, 'Designing with tokens from day one', 'mountains.webp'),
  post(5, 'A calmer content model for editors', 'workspace.webp'),
  post(4, 'Shipping a multilingual site in a week', 'city.webp'),
  post(3, 'A field guide to spacing', 'coast.webp'),
  post(2, 'Choosing a type pairing', 'abstract-square.webp'),
  post(1, 'Photographing for the web', 'forest-portrait.webp'),
]

async function fetchPage(page: number) {
  await new Promise((resolve) => setTimeout(resolve, 600))
  return all.slice((page - 1) * 3, page * 3)
}

export default function Demo() {
  return (
    <PostsList
      items={all.slice(0, 3)}
      view="grid3"
      loading="loadMore"
      fetchMore={fetchPage}
      paging={{ total: all.length, page: 1, limit: 3 }}
    />
  )
}
```

`loadMoreButton` styles the button: `variant` (a button variant, the default variant when unset), `title`, `iconBeforeHtml` / `iconAfterHtml` (icon markup strings) and `align` (`left`, `center` or `right`). In `pagination` mode the highlight and the lead grid appear on page 1 only; prefer `loadMore` or `infinite` for a lead/tail listing, because they append to the tail.

With a highlight or a lead grid, page 1 holds `limit` cards plus those extra ones and later pages hold `limit`. The [listing helpers](https://docs.systhema.app/ar/components/posts-helpers.md#listing-helpers) compute the offsets your `fetchMore` needs.

### Injecting items

`injectItems` is called after every card with `{ index, total, section, isLeadEnd }`; return a node to render it after that card, or `null`. In a split listing it is called once more with `isLeadEnd: true` between the lead grid and the tail, and that node renders full width in a `.posts-list-interspace` wrapper, outside both containers.

```tsx
<PostsList
  items={posts}
  leadCount={6}
  view="row"
  injectItems={({ index, isLeadEnd }) =>
    isLeadEnd ? <AdBanner /> : index > 0 && index % 8 === 0 ? <AdTile /> : null
  }
/>
```

### Custom cards

`CardIsland` and `HighlightIsland` replace the default cards with your own component, which receives `{ post }`. They must be client components (`'use client'`), and they handle their own linking. See [Customizing the presentation](https://docs.systhema.app/ar/payload/posts/presentation.md).

### Theme

`theme` sets `data-theme` on the list, so the cards resolve against that color-system mode. See [Themes and backgrounds](https://docs.systhema.app/ar/concepts/themes.md).

## Props

<!-- generated:props @systhemaui/react PostsListProps -->

| Prop                   | Type                                                                                                                                                                                                 | Default   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items` (required)     | `PostView[]`                                                                                                                                                                                         | -         | Server-rendered first page of resolved post view-models.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `view`                 | `PostsView`                                                                                                                                                                                          | `'grid4'` |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `highlightFirst`       | `boolean`                                                                                                                                                                                            | `false`   | When true, the first item renders as a full-bleed highlighted card (large media + bottom-left overlay) and the remainder flow into the grid.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `leadCount`            | `number`                                                                                                                                                                                             | -         | Optional **lead/tail split** (the Figma archive shape). When set to a positive number, the first `leadCount` post-highlight cards render in the `leadView` grid (the lead), and the remainder render below in the primary `view` container (the tail). Load-more appends to the **tail**. Unset / `0` / `>=` the item count ⇒ no split, the uniform single-container behaviour. Counts cards AFTER any `highlightFirst` hero — so `highlightFirst` + a `leadCount` of 6 renders: 1 hero, then a 6-card lead grid, then the tail.                                                                                                                                                                                                              |
| `leadView`             | `PostsView`                                                                                                                                                                                          | `'grid3'` | Lead grid layout when `leadCount` splits the listing (only when leadCount > 0).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `loading`              | `PostsLoading`                                                                                                                                                                                       | `'none'`  |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `fetchMore`            | `FetchMorePosts`                                                                                                                                                                                     | -         | Required for `pagination`/`loadMore`/`infinite` to fetch subsequent pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `paging`               | `PostsPaging`                                                                                                                                                                                        | -         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `injectItems`          | `((context: { index: number; total: number; section: 'lead' \| 'tail'; isLeadEnd: boolean; }) => ReactNode)`                                                                                         | -         | Interleave extra items (e.g. ads) into the rendered list. Called per render index; return a node to inject AFTER the card at that index, or null. Lets a consumer drop a leaderboard every N cards without PostsList knowing about ad shapes. `section` is `'lead'` or `'tail'` (always `'lead'` when the listing isn't split). `isLeadEnd` is true ONLY for the single seam call fired BETWEEN the lead grid and the tail list (when a split is active) — so a consumer can drop a full-width leaderboard at the grid→list seam with `({ section, isLeadEnd }) => section === 'lead' && isLeadEnd ? ad : null`, and it renders as a sibling between the two containers (never inside the grid, which would constrain it to a single column). |
| `className`            | `string`                                                                                                                                                                                             | -         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `gapX`                 | `GapSize` (`lg`, `md`, `sm`, `none`, `xs`, `xl`, `2xl`, `3xl`)                                                                                                                                       | -         | Optional per-axis inter-card gap (a design-system gap token size, `none` … `3xl`). `gapX` sets the column-gap, `gapY` the row-gap, applied as inline styles on the grid/row container — overriding the design-default gaps (the core-CSS asymmetric grid gaps, or the `.posts-list-row` row gap) on that axis only. Omit either to keep that axis's default.                                                                                                                                                                                                                                                                                                                                                                                  |
| `gapY`                 | `GapSize` (`lg`, `md`, `sm`, `none`, `xs`, `xl`, `2xl`, `3xl`)                                                                                                                                       | -         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `theme`                | `ColorSystem` (`dark`, `default`)                                                                                                                                                                    | -         |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `renderImage`          | `((media: { url: string; alt?: string \| undefined; width?: number \| undefined; height?: number \| undefined; srcset?: string \| undefined; isPlaceholder?: boolean \| undefined; }) => ReactNode)` | -         | Forwarded to each PostCard (the next package passes a next/image renderer).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `renderHighlightImage` | `((media: { url: string; alt?: string \| undefined; width?: number \| undefined; height?: number \| undefined; srcset?: string \| undefined; isPlaceholder?: boolean \| undefined; }) => ReactNode)` | -         | Forwarded to the HighlightCard (next passes a `next/image` fill renderer).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `cardConfig`           | `ResolvedCardConfig`                                                                                                                                                                                 | -         | Resolved Tier-1 card config for the main/tail (`view`) layout (show/classNames).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `leadCardConfig`       | `ResolvedCardConfig`                                                                                                                                                                                 | -         | Resolved Tier-1 card config for the LEAD (`leadView`) layout when a lead/tail split is active. Falls back to {@link cardConfig} when unset, so an un-split listing (or a caller that doesn't distinguish sections) is unaffected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `highlightConfig`      | `ResolvedHighlightCardConfig`                                                                                                                                                                        | -         | Resolved Tier-1 highlight-card config (show/classNames incl. overlay).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `loadMoreButton`       | `{ variant?, title?, iconBeforeHtml?, iconAfterHtml?, align? }`                                                                                                                                      | -         | Optional configuration for the **Load more** button (only used when `loading === 'loadMore'`). The button renders as a real design-system `<Button>` carrying the chosen variant (unset ⇒ the design-system default variant). `title` overrides the "Load more" label; `iconBeforeHtml` / `iconAfterHtml` are pre-resolved icon HTML strings injected via `dangerouslySetInnerHTML` (the payload layer resolves icon picks to HTML); `align` positions the actions row (`left` / `center` / `right`, default `center`). The action itself (fetch next page) is unchanged.                                                                                                                                                                     |
| `CardIsland`           | `PostCardIsland`                                                                                                                                                                                     | -         | Tier-2 card island (`'use client'`). When set it replaces the default card.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `HighlightIsland`      | `PostCardIsland`                                                                                                                                                                                     | -         | Tier-2 highlight island (`'use client'`). When set it replaces the default.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `LinkComponent`        | `ElementType`                                                                                                                                                                                        | -         | Wrapper used to link each default card/highlight to its `post.href`. Defaults to a plain `<a>`; the next package passes a `next/link` for client navigation + prefetch. The wrapper carries `display: contents` so it doesn't disturb the grid/flex layout. Islands are NOT wrapped — they own their own linking.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `disableAnimation`     | `boolean`                                                                                                                                                                                            | -         | Disable per-card scroll-reveal animation. By default each card/highlight animates **individually** as it enters the viewport; the wrapping Section is expected to NOT animate (so the whole stack doesn't fade in together).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `dividers`             | `boolean`                                                                                                                                                                                            | `false`   | Draw a 1px `--color-separator-line` hairline between adjacent rows in the `row` (and split lead `row`) containers — the Figma archive list shape. Grids are unaffected (a row hairline doesn't apply across columns). Defaults to `false` (today's plain flex-gap container).                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `labels`               | `{ loadMore?, loading?, previous?, next?, readMore?, pagination?, empty? }`                                                                                                                          | -         | Labels for the interactive controls (override for i18n).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |

<!-- /generated -->

Two props are not obvious from their types:

- `LinkComponent` wraps each default card and the highlight in a link to `post.href`, with `display: contents` so the link does not take part in the grid. It defaults to `<a>` in React and to `next/link` in Next.js. Islands are never wrapped.
- `labels` translates the built-in text: `loadMore`, `loading`, `previous`, `next`, `readMore`, `pagination` (the pagination landmark's name) and `empty` (default "No posts found.").

## Reveal

Each card reveals on its own as it scrolls in. The list root and the divider container are `<Stack allowChildAnimation>`, because a plain [`Stack`](https://docs.systhema.app/ar/components/stack.md) cancels the reveals inside it. In a grid row the cards cascade left to right: the `posts` CSS layer gives each column one delay step of `var(--tw-stagger-delay, 120ms)`, only at the breakpoints where that column count applies. One-column grids and the `row` view are not staggered. Set `--tw-stagger-delay` to retune the step, or pass `disableAnimation` to turn the reveals off. See [Motion](https://docs.systhema.app/ar/concepts/motion.md).

## HTML and CSS

The layout comes from the `posts` CSS layer (`blocks.posts`, on by default), not from Tailwind grid utilities, so it renders in any project whatever Tailwind scans. The layer styles `.posts-list*`, `.post-card*`, `.post-highlight*`, `.post-meta*`, `.archive-filter*` and `.post-share*`. Turn it off with `blocks: { posts: false }` only when the project does not use posts. See [Component CSS blocks](https://docs.systhema.app/ar/styling/blocks.md).

```html
<div class="stack flex flex-col gap-md aos-allow-children posts-list posts-list-grid3">
  <div class="posts-list-grid posts-list-grid-3">
    <a class="post-card-link contents" href="/journal/designing-with-tokens">
      <article class="post-card post-card-grid3 flex flex-col gap-4">
        <div class="post-card-media media-wrapper aspect-[4/3]">
          <img src="/media/mountains.webp" alt="" loading="lazy" />
        </div>
        <div class="post-card-body flex flex-col gap-4">
          <span class="post-card-label text-label">Design</span>
          <h3 class="post-card-title color-heading text-h4">Designing with tokens from day one</h3>
          <div class="post-meta post-card-meta text-small flex flex-wrap items-center gap-x-2">
            <span class="post-meta-author">Mira Kovač</span>
            <span class="post-meta-separator" aria-hidden="true">•</span>
            <time class="post-meta-date" datetime="2026-09-14">September 14, 2026</time>
          </div>
          <p class="post-card-excerpt color-body text-body line-clamp-3">How a small team…</p>
        </div>
      </article>
    </a>
  </div>
</div>
```

The list is at most `--container-width` wide and centered, so it does not bleed edge to edge outside a `Section`. The card root sets its color from `--color-typography-body`, so the title, excerpt and byline follow the active color system on a themed band.

The per-column stagger lives in this layer and out-declares the reveal animation's own `animation` shorthand. To override the delay from your own CSS, use the `app` layer (or `!important`), not a more specific selector. See [Cascade layers](https://docs.systhema.app/ar/styling/cascade-layers.md).

<!-- generated:utilities posts -->

| Class                               | Styles                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `posts-list`                        | `width: 100%;`<br>`max-width: var(--container-width);`<br>`margin-inline: auto;`                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `posts-list-grid`                   | `& { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: var(--gap-sm, 1.25rem); row-gap: var(--gap-md, 2.5rem); }`<br>`& > .post-card { animation-delay: var(--post-card-stagger-delay, 0ms); }`<br>`& > * > .post-card { animation-delay: var(--post-card-stagger-delay, 0ms); }`                                                                                                                                                                                                                   |
| `posts-list-row`                    | `display: flex;`<br>`flex-direction: column;`<br>`row-gap: var(--gap-md, 2.5rem);`                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `posts-list-grid-2`                 | `@media (width >= 640px) { & { grid-template-columns: repeat(2, minmax(0, 1fr)); } }`<br>`@media (width >= 640px) { & > :nth-child(2n - 1) { --post-card-stagger-delay: 0ms; } }`<br>`@media (width >= 640px) { & > :nth-child(2n) { --post-card-stagger-delay: calc(var(--tw-stagger-delay, 120ms) * 1); } }`                                                                                                                                                                                                      |
| `posts-list-grid-3`                 | `@media (width >= 640px) { & { grid-template-columns: repeat(2, minmax(0, 1fr)); } }`<br>`@media (width >= 1192px) { & { grid-template-columns: repeat(3, minmax(0, 1fr)); } }`<br>`@media (width >= 640px) and (width < 1192px) { & > :nth-child(2n - 1) { --post-card-stagger-delay: 0ms; } }`<br>`@media (width >= 640px) and (width < 1192px) { & > :nth-child(2n) { --post-card-stagger-delay: calc(var(--tw-stagger-delay, 120ms) * 1); } }`<br>+ 3 more rules                                                |
| `posts-list-grid-4`                 | `@media (width >= 640px) { & { grid-template-columns: repeat(2, minmax(0, 1fr)); } }`<br>`@media (width >= 1192px) { & { grid-template-columns: repeat(4, minmax(0, 1fr)); } }`<br>`@media (width >= 640px) and (width < 1192px) { & > :nth-child(2n - 1) { --post-card-stagger-delay: 0ms; } }`<br>`@media (width >= 640px) and (width < 1192px) { & > :nth-child(2n) { --post-card-stagger-delay: calc(var(--tw-stagger-delay, 120ms) * 1); } }`<br>+ 4 more rules                                                |
| `posts-list-interspace`             | `width: 100%;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `posts-list-empty`                  | `color: color-mix(in srgb, currentColor 60%, transparent);`<br>`text-align: center;`<br>`padding-block: var(--gap-md, 2.5rem);`                                                                                                                                                                                                                                                                                                                                                                                     |
| `posts-list-actions`                | `display: flex;`<br>`align-items: center;`<br>`justify-content: center;`<br>`gap: var(--gap-sm, 1.25rem);`                                                                                                                                                                                                                                                                                                                                                                                                          |
| `posts-list-load-more`              | `&:disabled, .posts-list-load-more[disabled] { opacity: 0.4; cursor: not-allowed; }`                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `posts-list-pagination`             | `& { display: flex; justify-content: center; align-items: center; gap: var(--gallery-navigation-gap-x, 10px); }`<br>+ 13 more rules                                                                                                                                                                                                                                                                                                                                                                                 |
| `post-card`                         | `& { color: var(--color-typography-body); }`<br>`.posts-list-grid > .post-card { animation-delay: var(--post-card-stagger-delay, 0ms); }`<br>`.posts-list-grid > * > .post-card { animation-delay: var(--post-card-stagger-delay, 0ms); }`                                                                                                                                                                                                                                                                          |
| `post-card-label`                   | `& { color: color-mix(in srgb, currentColor 60%, transparent); }`<br>`&.text-label { color: color-mix(in srgb, currentColor 60%, transparent); }`                                                                                                                                                                                                                                                                                                                                                                   |
| `post-highlight-label`              | `& { color: color-mix(in srgb, currentColor 60%, transparent); }`<br>`&.text-label { color: color-mix(in srgb, currentColor 60%, transparent); }`                                                                                                                                                                                                                                                                                                                                                                   |
| `post-type`                         | `color: color-mix(in srgb, currentColor 60%, transparent);`                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `text-label`                        | `& { font-family: var(--font-label-font-family); font-size: var(--typography-label-font-size); font-weight: var(--font-label-font-weight); letter-spacing: var(--typography-label-letter-spacing); line-height: var(--typography-label-line-height); text-transform: uppercase; text-decoration: none; font-style: var(--font-label-font-style); }`<br>`.post-card-label.text-label { color: color-mix(in srgb, currentColor 60%, transparent); }`<br>+ 1 more rule                                                 |
| `post-card-title`                   | `text-wrap: balance;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `post-card-read-more`               | `color: var(--color-link-text-default, currentColor);`<br>`font-weight: 600;`<br>`text-decoration-line: underline;`                                                                                                                                                                                                                                                                                                                                                                                                 |
| `post-highlight-read-more`          | `color: var(--color-link-text-default, currentColor);`<br>`font-weight: 600;`<br>`text-decoration-line: underline;`                                                                                                                                                                                                                                                                                                                                                                                                 |
| `post-card-media-placeholder`       | `width: 100%;`<br>`height: 100%;`<br>`background-color: color-mix(in srgb, currentColor 6%, transparent);`                                                                                                                                                                                                                                                                                                                                                                                                          |
| `post-card-row`                     | `@media (width >= 640px) { & .post-card-body { align-self: start; justify-content: flex-start; } }`                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `post-card-body`                    | `@media (width >= 640px) { .post-card-row .post-card-body { align-self: start; justify-content: flex-start; } }`                                                                                                                                                                                                                                                                                                                                                                                                    |
| `post-highlight`                    | `position: relative;`<br>`overflow: hidden;`<br>`border-radius: var(--card-default-border-radius, 12px);`                                                                                                                                                                                                                                                                                                                                                                                                           |
| `highlight-card`                    | `position: relative;`<br>`overflow: hidden;`<br>`border-radius: var(--card-default-border-radius, 12px);`                                                                                                                                                                                                                                                                                                                                                                                                           |
| `post-highlight-scrim`              | `position: absolute;`<br>`inset: 0;`<br>`background: linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.35) 35%, rgba(0,0,0,0) 70%);`<br>`pointer-events: none;`                                                                                                                                                                                                                                                                                                                                             |
| `post-highlight-overlay`            | `position: relative;`<br>`z-index: 1;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `post-highlight-excerpt`            | `color: color-mix(in srgb, #ffffff 85%, transparent);`                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `post-meta`                         | `display: flex;`<br>`align-items: center;`<br>`flex-wrap: wrap;`<br>`gap: 0.5em;`<br>`color: color-mix(in srgb, currentColor 60%, transparent);`                                                                                                                                                                                                                                                                                                                                                                    |
| `post-byline-meta`                  | `display: flex;`<br>`align-items: center;`<br>`flex-wrap: wrap;`<br>`gap: 0.5em;`<br>`color: color-mix(in srgb, currentColor 60%, transparent);`                                                                                                                                                                                                                                                                                                                                                                    |
| `post-meta-avatar`                  | `width: var(--post-byline-avatar-size, 1.75em);`<br>`height: var(--post-byline-avatar-size, 1.75em);`<br>`border-radius: 9999px;`<br>`object-fit: cover;`<br>`flex-shrink: 0;`                                                                                                                                                                                                                                                                                                                                      |
| `post-meta-separator`               | `opacity: 0.5;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `post-meta-author-role`             | `& { opacity: 0.7; }`<br>`&::before { content: ", "; }`                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `archive-filter`                    | `display: flex;`<br>`flex-direction: column;`<br>`gap: var(--gap-sm, 1.25rem);`                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `archive-filter-bar`                | `display: flex;`<br>`flex-wrap: wrap;`<br>`align-items: center;`<br>`gap: var(--gap-sm, 1.25rem);`                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `archive-filter-label`              | `flex: 0 0 auto;`<br>`color: color-mix(in srgb, currentColor 60%, transparent);`                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `archive-filter-search`             | `flex: 1 1 16rem;`<br>`min-width: 0;`<br>`margin: 0;`                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `archive-filter-select`             | `flex: 1 1 16rem;`<br>`min-width: 0;`<br>`margin: 0;`                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `archive-filter-chips`              | `display: flex;`<br>`flex-wrap: wrap;`<br>`align-items: center;`<br>`gap: var(--chip-gap, 0.5rem);`<br>`flex: 1 1 16rem;`<br>`min-width: 0;`                                                                                                                                                                                                                                                                                                                                                                        |
| `archive-filter-chip-toggle`        | `cursor: pointer;`<br>`appearance: none;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `archive-filter-chip-toggle-active` | `background-color: var(--color-chip-hover-background);`<br>`border-color: var(--color-chip-hover-border);`<br>`color: var(--color-chip-hover-text);`                                                                                                                                                                                                                                                                                                                                                                |
| `archive-filter-active`             | `display: flex;`<br>`flex-wrap: wrap;`<br>`align-items: center;`<br>`gap: var(--chip-gap, 0.5rem);`                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `archive-filter-chip`               | `& { appearance: none; cursor: pointer; display: inline-flex; align-items: center; gap: var(--chip-gap); font-size: var(--chip-font-size); line-height: var(--chip-line-height); letter-spacing: calc(var(--chip-letter-spacing, 0) * 1px); padding-inline: var(--chip-padding-x); padding-block: var(--chip-padding-y); border-radius: var(--chip-border-radius); border-style: solid; border-width: var(--chip-border-width); background-color: var(--color-chip-normal-background); border-co…`<br>+ 1 more rule |
| `archive-filter-chip-remove`        | `font-size: 1.2em;`<br>`line-height: 1;`<br>`opacity: 0.7;`                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `post-related-title`                | `margin-bottom: var(--gap-sm, 1.25rem);`                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `post-single`                       | `& .article > :not(section, .figure-w-full, .figure-w-screen, .figure-w-container, a:not([class*="card"]), div:not(.columns, .stack, [class*="card"], [class*="accordion"])) { max-width: var(--post-reading-width, calc(var(--container-width) - (var(--article-padding-x) * 2))); }`                                                                                                                                                                                                                              |
| `post-share-button-label`           | `font-size: var(--text-small-font-size, 0.875rem);`<br>`line-height: 1;`                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `post-share-icon-label`             | `& .post-share-list > a { display: inline-flex; align-items: center; gap: var(--gap-xs, 0.5rem); width: auto; }`<br>`& .post-share-list > button { display: inline-flex; align-items: center; gap: var(--gap-xs, 0.5rem); width: auto; }`                                                                                                                                                                                                                                                                           |
| `post-share-list`                   | `.post-share-icon-label .post-share-list > a { display: inline-flex; align-items: center; gap: var(--gap-xs, 0.5rem); width: auto; }`<br>`.post-share-icon-label .post-share-list > button { display: inline-flex; align-items: center; gap: var(--gap-xs, 0.5rem); width: auto; }`<br>`.post-share-pills .post-share-list > a { display: inline-flex; align-items: center; gap: var(--gap-xs, 0.5rem); width: auto; }`<br>+ 3 more rules                                                                           |
| `post-share-pills`                  | `& .post-share-list > a { display: inline-flex; align-items: center; gap: var(--gap-xs, 0.5rem); width: auto; }`<br>`& .post-share-list > button { display: inline-flex; align-items: center; gap: var(--gap-xs, 0.5rem); width: auto; }`<br>`& .post-share-list > a { padding: var(--gap-xs, 0.5rem) var(--gap-sm, 1.25rem); border-radius: 9999px; border: 1px solid var(--color-separator-line, currentColor); }`<br>+ 1 more rule                                                                               |
| `post-share-sticky-bottom`          | `position: fixed;`<br>`left: 0;`<br>`right: 0;`<br>`bottom: 0;`<br>`z-index: 40;`<br>`justify-content: center;`<br>`background-color: var(--color-foundations-surface-bg);`<br>`border-top: 1px solid var(--color-separator-line, currentColor);`<br>`padding-block: var(--gap-sm, 1.25rem);`                                                                                                                                                                                                                       |
| `post-share-floating-sidebar`       | `@media (width >= 1192px) { & { position: fixed; left: var(--gap-lg, 1.5rem); top: 50%; transform: translateY(-50%); flex-direction: column; align-items: center; z-index: 40; } }`                                                                                                                                                                                                                                                                                                                                 |

<!-- /generated -->

## Next.js

The `@systhemaui/next` `PostsList` has the same props as the React one and fills in two defaults:

- Cards render media with `next/image` (`fill`, `objectFit="cover"`) and a `sizes` hint from [`sizesForLayout(view)`](https://docs.systhema.app/ar/components/posts-helpers.md#sizesforlayout).
- The highlight card renders `fill`, `sizes="100vw"` and `priority`, so it loads eagerly as the likely LCP image.
- Each card links through `next/link`.

A `renderImage`, `renderHighlightImage` or `LinkComponent` you pass wins over these defaults. The component is a client component; use it from server components with serializable props, and from a client module when you pass `fetchMore`, `injectItems` or islands.

In a Payload project, `@systhemaui/payload/next` also exports the archive pipeline (`resolveArchiveData`, `PostsListClient`, `ArchiveFilterProvider`, `useArchiveFilters`, `ArchiveFilterControl`) for building a custom archive with search, load more and ads. See [Archive pages](https://docs.systhema.app/ar/payload/posts/archives.md#building-a-custom-archive-search--load-more--ads-together).

## Accessibility

- The list sets `aria-busy` while a page loads.
- In `loadMore` and `infinite` modes a visually hidden `role="status"` region announces the loading text, then the `shown / total` count after each page.
- The empty state ("No posts found.") is an `aria-live="polite"` region.
- Pagination is a `<nav>` named by `labels.pagination`; its buttons have visually hidden "Previous" and "Next" text, and changing page scrolls the top of the listing into view.
- The whole card is one link, so its text (label, title, byline and excerpt) is the link's name. Keep titles short and descriptive.
- Card titles are `<h3>` and the highlight title is `<h2>`. When the page outline needs other levels, render the standalone cards with `headingLevel`, or use an island.

## Related

- [PostCard and HighlightCard](https://docs.systhema.app/ar/components/post-card.md)
- [Posts view model and helpers](https://docs.systhema.app/ar/components/posts-helpers.md)
- [ArchiveSearchFilter](https://docs.systhema.app/ar/components/archive-search-filter.md)
- [Posts block](https://docs.systhema.app/ar/payload/blocks/posts.md)
- [Listings and sharing](https://docs.systhema.app/ar/payload/posts/listing-and-sharing.md)
