Skip to content

Components / Sections

Blog Hover List

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.

Live demo

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Variants · each row writes the attribute the prop writes; the section's own CSS answers

tone
density

Measured

JavaScript of its own

0 B

Uses HoverImageLinks and the shared runtime (1.9 kB raw, once per site). With those included: 2.3 kB raw.

CSS 11.0 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

Chrome 105 · Firefox 121 · Safari 15.4; needs has. Elsewhere: without :has() the cover does not show; the list works as it is.

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/blog-hover-list
src/pages/index.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' }}
/>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: hover-image-links, underline-hover.

Props

PropTypeDefaultDescription
posts{ href: string; title: string; date: string | Date; minutes?: number; cover?: string | { src, srcset?, width?, height? } }[]noneThe 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.
headlinestringnoneThe section's heading. Slot "headline" wins.
eyebrowstringnoneA short line above the headline, in mono. Slot "eyebrow" wins.
copystringnoneA paragraph under the headline. Slot "copy" wins.
level1 | 22Heading level of the headline: 2 under a page's h1, 1 when the list opens its page.
labelstring'Posts'Accessible name of the list.
minutesLabelstring'{n} min read'Reading-time text, {n} where the number goes.
localestringnoneIntl 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 }noneA link after the list, such as every post. Slot "actions" wins.
sizenumber320Width of the floating cover in px.
ratiostring'16 / 10'Aspect ratio of the floating cover.
morphbooleantrueName 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.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-hoverlist.ma-hoverlist__actions.ma-hoverlist__copy.ma-hoverlist__date.ma-hoverlist__eyebrow.ma-hoverlist__head.ma-hoverlist__headline.ma-hoverlist__inner.ma-hoverlist__item.ma-hoverlist__link.ma-hoverlist__list.ma-hoverlist__minutes.ma-hoverlist__more.ma-hoverlist__title

Slots

slot="eyebrow"slot="headline"slot="copy"slot="actions"

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.

Why it is built this way

Replaces: Blog lists (Framer portfolio templates) · HoverImageLinks blog index (Hover.dev) · Writing list (Awwwards portfolios). See the migration table.

A link underline that enters from the left on hover and leaves to the right, so the exit continues the entrance instead of rewinding it.

every browserhover
All posts

Sample post: a page built for reading

This post is sample text. It describes the page it sits on, so every sentence can be checked against what the page does.

What follows you as you read

Two things on this page follow the reader: the line beside the text and the list of headings in the side column.

The beam

The line fills as the article scrolls past. It runs on the article's own view timeline, so the browser draws it and no script counts pixels.

The contents list

The list marks the heading you are reading. Its links are plain anchors, and a jump stops below the sticky bar.

Components a post brings

A post imports what it uses: a marker, code tabs, a gallery and a link card.

npx astro add moonarc
pnpm astro add moonarc
Sample picture 1, a gradient studySample picture 2, a gradient studySample picture 3, a gradient study

The list beside this text is Reading TocReading TocA table of contents that marks the heading you are reading., and the line is its sibling.

Without JavaScript

The line is full and still, the list is a list of links, the pictures link to their full size, and the card is a plain link.

The page of one blog post: a header with the date, the reading time, the title, the author, the tags and the cover, then the body in a measured column that a beam fills as it is read, beside a contents list that follows the reader.

Chrome 115 · not Firefox · Safari 26scrollclick
Release

Endpoint, Homescreen, Estate, Practice and Stay, templates for developer products, mobile apps, estate agents, practices and places to stay, and thirty sections

Endpoint, a template for a product with an HTTP API: a console on the home page that sends a request and streams the response back with its status, its time and its region, an API reference written at build from your OpenAPI document, a quickstart and four guides, usage pricing with a slider, a status page, the regions on a map, a changelog with a feed, search and a contact page for sales.

Shows blog posts as cards, with the first post featured across two columns.

newly · Chrome 111 · Firefox 144 · Safari 18scrollhoverclick

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown