Hover Image Links
PointerA list of project links: hovering one floats its image beside the pointer, leaning the way the pointer moves, while the other links step back.
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, 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
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
Chrome 105 · Firefox 121 · Safari 15.4; needs has. Elsewhere: without :has() the cover does not show; the list works as it is.
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:
npx shadcn@latest add @moonarc-pro/blog-hover-list---
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.
| 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. |
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.
.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
slot="eyebrow"slot="headline"slot="copy"slot="actions"
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.
ClientRouterHoverImageLinks binds through the shared runtime and is released before the swap; with morph, the cover grows into the post's own.
Replaces: Blog lists (Framer portfolio templates) · HoverImageLinks blog index (Hover.dev) · Writing list (Awwwards portfolios). See the migration table.
A list of project links: hovering one floats its image beside the pointer, leaning the way the pointer moves, while the other links step back.
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.
This post is sample text. It describes the page it sits on, so every sentence can be checked against what the page does.
Two things on this page follow the reader: the line beside the text and the list of headings in the side column.
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 list marks the heading you are reading. Its links are plain anchors, and a jump stops below the sticky bar.
A post imports what it uses: a marker, code tabs, a gallery and a link card.
npx astro add moonarcpnpm astro add moonarcThe list beside this text is Reading TocReading TocA table of contents that marks the heading you are reading., and the line is its sibling.
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.
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.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown