Skip to main content

Catalog · Content

Post list

Lays out a list of posts as a grid, with the latest one larger, or as a compact list of rows. Each post is a card.

Fits best: blog — a page that lists posts: the home, a category, an archive, search results

Example

The same posts in the three layouts. Each one is a card.

The base: as many columns as fit, none under 16rem.

luz-post-list--lead: the first post spans two columns with its picture beside it, when the list is 40rem wide or more. Narrower, it is a card like the rest.

luz-post-list--compact: one row for each post, the title and the facts, with no picture.

3 results for "tide"

Search results: the search box, then a heading that says how many, then the compact list. The posts, the word searched and the counts are placeholders: your search fills them in.

Known issues

None known.

Code

Copy the markup

Bundler
import "@luz-ds/catalog/components/post-list.css";
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/post-list.min.css" integrity="sha384-kK6zd16AVte8C1ge85pYFbh52scr+Tf3HNyUDxai1uQkIAT9+/5HJOWZZ/zzFn8H" crossorigin="anonymous">

A list, one card in each item. The first item of the lead list has its text in a <div> beside the picture:

HTML
<ul class="luz-post-list luz-post-list--lead" role="list">
  <li>
    <article class="luz-card">
      <div class="luz-image luz-image--3x2"><img src="/media/tide.jpg" width="600" height="400" alt="" loading="lazy"></div>
      <div>
        <p class="luz-tag luz-tag--gray u-mb-sm">Coast</p>
        <h2 class="luz-card__title"><a href="/2026/10/tide-records/">What ten years of tide records say about the bay</a></h2>
        <p class="luz-card__body">Volunteers have read the gauge in the port twice a day since 2016.</p>
        <p class="luz-meta"><time datetime="2026-10-12">12 October 2026</time> <span>6 min read</span></p>
      </div>
    </article>
  </li>
  <!-- one li for each post; the others have no div around their text -->
</ul>

A template for the results page of a search: a <search> with its form, a heading that says how many results there are, and the compact list. Your site's search fills in the results; the text here is a placeholder.

HTML
<search>
  <form action="/search/">
    <label for="q">Search the blog</label>
    <input type="search" id="q" name="q" value="tide">
    <button type="submit">Search</button>
  </form>
</search>
<h2 id="results-title">3 results for "tide"</h2>
<ul class="luz-post-list luz-post-list--compact" role="list" aria-labelledby="results-title">
  <li>
    <article class="luz-card">
      <h3 class="luz-card__title"><a href="/2026/10/tide-records/">What ten years of tide records say about the bay</a></h3>
      <p class="luz-meta"><time datetime="2026-10-12">12 October 2026</time> <span>Coast</span></p>
    </article>
  </li>
  <!-- one li for each result -->
</ul>

Variants and options

ClassEffect
.luz-post-list
On a <ul>: as many equal columns as fit, none under --luz-post-list-min (16rem), no bullets. Each card is as tall as the tallest in its row
.luz-post-list--lead
From 40rem of list width, the first item spans two columns and its card puts the picture beside the text, with a larger title
.luz-post-list--compact
One column, a row for each post: the title, and the facts at its end when there is room. Leave out the picture and the summary
--luz-post-list-min
The narrowest a column may be. Set it on the list or an ancestor

How it works

CSS from components/post-list.css of @luz-ds/catalog. It sets the layout; the post is core's card, with the picture, tag, title, summary and meta line you choose.

  • The list is its own size container, so the lead follows the room it has and not the window. Give it a parent that sets its width: a flex or grid item sized by its content leaves it no width.
  • Under 40rem the lead is a card like the rest, with its picture above.
  • The link on the title makes the whole card one link, as in any card.

Without JavaScript

Nothing changes. It is a list of cards, and the package has no script.

Keyboard and ARIA

  • Order: the source order, which is the reading order. In the lead the picture comes before the text; each card is one tab stop, on its title link.
  • List: the <ul> is announced with its count ("list, 12 items"). Keep role="list": some browsers drop the list from a <ul> without bullets.
  • Names: each link is named by its post title. A picture that only decorates the post has alt="". Give the list a heading, or an aria-label, when the page has more than one.
  • Search results: <search> is the search landmark, so the form inside it needs no role="search": adding one would nest a second landmark. Put the results outside <search>, in the <main>. If the page has more than one <search>, name each with aria-label. Name the list with the heading that counts the results, with aria-labelledby.
  • Announcing the results: a results page that loads whole needs no live region: put the count and the query in the <title> and in the <h1>. If your site filters the list in place, put the count in a separate element with role="status": a heading is not live, so a screen reader does not say it changed.

Contrast and focus are in the Design tab.

Design

When to use

Use it whenever a page shows several posts, so every list on the site is laid out alike.

  • Base: the posts of a category or a tag.
  • Lead: the home, when one post should be read first.
  • Compact: an archive by date, or the results of a search.

When not to use

Do not use it for a single card, or for items that are not posts.

  • One post is a card. Products, people and plans are cards in a .o-grid--auto.
  • For the pages of a long list, add pagination after it.

Do and don't

Do Keep a compact row to the title and its facts: a long archive can be scanned at a glance.
Don't Put a summary in a compact row: it breaks the row in two and the archive stops being scannable.

Writing the content

  • Title: the title of the post, as on the post. Wrap, never cut.
  • Summary: one sentence, up to about 120 characters. Leave it out in the lead's side posts and in the compact list.
  • Facts: the date, as a <time>, then the reading time or the section. Same order in every card.
  • Heading level: the card title is a heading one level under the heading of the list's page or section.
  • Search results: say how many results there are and for what (3 results for "tide"), in the heading, the <title> and the <h1> of a page that loads whole. With none, say so and how to go on: check the spelling, or try a shorter word.

Contrast and focus

The cards sit on the page surface, with the same colours as any card.

PartColour on backgroundLightDarkHigh contrastNeeds
Title
--luz-brand-primary on --luz-surface
5.9:1
7.2:1
14.1:1
4.5:1
Facts
--luz-text-secondary on --luz-surface
6.4:1
6.9:1
19.3:1
4.5:1
Border under the pointer
--luz-input-border on --luz-surface-page
3.7:1
4.2:1
21.0:1
3:1
Focus ring
--luz-focus-ring on --luz-surface
5.9:1
7.2:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: the ring is the card's: 2 px of --luz-focus-ring around the whole card, from the link on the title.
  • Target: the whole card is the link, far over 24 px.
  • Edges: the border at rest (--luz-divider) is decorative and has no minimum.

Tokens it uses

The 5 tokens that the .luz-post-list rules of catalog’s components/post-list.css read, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-space-3xs, --luz-space-md, --luz-space-sm, --luz-space-xs
Typography
--luz-text-xl

Keyboard and ARIA are in the Code tab.