# Blog Hover List (Moonarc)

Writing as a list: the day, the title at display size and the reading time on one line per post, the whole line a link, and the post's cover floating by the pointer while the line is hovered. The float, its lag and its lean are HoverImageLinks (each line picks its cover through data-hil); the title underlines with UnderlineHover; the other lines step back. Keyboard focus shows the cover at a fixed spot and touch gets the plain list. Each cover carries the view transition name BlogArticle gives the same post's cover, so with Astro's router it grows into the post. Zero JavaScript of its own.

- Pro block. Install: `npx shadcn@latest add @moonarc-pro/blog-hover-list` (license key required)
- Tier B · category section · trigger hover, pointer
- Readout: `<BlogHoverList posts={[{ href, title, date, minutes, cover }]}>`
- Browser support: widely (Chrome 105 · Firefox 121 · Safari 15.4); elsewhere: without :has() the cover does not show; the list works as it is
- Measured cost: 0 B JS of its own · uses HoverImageLinks + runtime (with dependencies 2.3 kB raw; CSS 11.0 kB raw)
- Page: https://moonarc.dev/components/blog-hover-list/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `posts` | `{ href: string; title: string; date: string | Date; minutes?: number; cover?: string | { src, srcset?, width?, height? } }[]` | none | The posts, in the order to show them. The cover floats by the pointer; its alt is empty, since the title already names the link. A post without one is a line like the others. |
| `headline` | `string` | none | The section's heading. Slot "headline" wins. |
| `eyebrow` | `string` | none | A short line above the headline, in mono. Slot "eyebrow" wins. |
| `copy` | `string` | none | A paragraph under the headline. Slot "copy" wins. |
| `level` | `1 | 2` | `2` | Heading level of the headline: 2 under a page's h1, 1 when the list opens its page. |
| `label` | `string` | `'Posts'` | Accessible name of the list. |
| `minutesLabel` | `string` | `'{n} min read'` | Reading-time text, {n} where the number goes. |
| `locale` | `string` | none | Intl locale of a Date; unset, the runtime's. |
| `dateStyle` | `'full' | 'long' | 'medium' | 'short'` | `'medium'` | How a Date prints (Intl.DateTimeFormat, in UTC, so the day the file names); a string prints as given. |
| `more` | `{ href: string; label: string }` | none | A link after the list, such as every post. Slot "actions" wins. |
| `size` | `number` | `320` | Width of the floating cover in px. |
| `ratio` | `string` | `'16 / 10'` | Aspect ratio of the floating cover. |
| `morph` | `boolean` | `true` | Name each cover ma-cover-<href>, the name BlogArticle gives the post's own cover, so the cover a reader clicked grows into the post under Astro's router. Turn it off when another section on the page names the same posts (a BlogGrid). |
| `tone` | `'surface' | 'muted' | 'ink' | 'brand'` | `unset (paints nothing)` | The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. |
| `density` | `'compact' | 'cozy' | 'roomy'` | `'cozy'` | Block padding and the lines' height ×0.8 / ×1 / ×1.25 through --ma-density. |

## Usage

```astro
---
import BlogHoverList from '@/components/moonarc/pro/BlogHoverList.astro';
import { getImage } from 'astro:assets';
import cover from '../assets/posts/grid.webp';

// a cover through astro:assets: the float loads the size it shows
const img = await getImage({ src: cover, widths: [320, 640], width: 640 });
---
<BlogHoverList
  headline="Writing"
  posts={[
    { href: '/writing/grids/', title: 'Grids that bend on purpose', date: new Date('2026-09-02'), minutes: 6, cover: { src: img.src, srcset: img.srcSet.attribute } },
    { href: '/writing/motion-budgets/', title: 'A motion budget for a small site', date: new Date('2026-08-14'), minutes: 4 },
  ]}
  more={{ href: '/writing/', label: 'Every post' }}
/>
```

## Reduced motion

The cover appears at a fixed spot on the right of the list while a line is hovered or focused, with no follow and no lean (HoverImageLinks), and the underline appears without its sweep.

## With ClientRouter

HoverImageLinks binds through the shared runtime and is released before the swap; with morph, the cover grows into the post's own.

## Craft

- The line is the link: the day, the title and the time all sit in one anchor, so the target is the whole row and a screen reader hears the three in order.
- The day and the reading time are mono at the secondary strength, the title in the display face: the eye runs down the titles and the numbers stay out of the way.
- The covers float at 16:10 and 320 px by default, loaded at that width from a srcset: a list of twelve posts does not load twelve full covers.
- Under 44 rem of the section's width the day and the time share a line above the title, so a long title keeps the whole width.
- The other lines keep their strength while one is hovered: the cover and the underline already say which line the pointer is on, and a dimmed line would drop its small day and time under 4.5 : 1.

## Replaces

- Blog lists (Framer portfolio templates)
- HoverImageLinks blog index (Hover.dev)
- Writing list (Awwwards portfolios)

## Source

Pro block. The source is served by the license-gated registry: `npx shadcn@latest add @moonarc-pro/blog-hover-list` with a key in components.json (https://moonarc.dev/account/). Everything it composes is free: hover-image-links, underline-hover.
