Cover Image

PreviousNext

An image that never shows a broken frame — a missing src, a 404 or a mixed-content block all resolve to an elevated fallback, not a flat placeholder.

The Great Gatsby
loads
To Kill a Mockingbird
colour placeholder
1984
http:// — sanitize on
no src
Infinite Jest
src that 404s
Brave New World
loads

The same missing cover at two depths — the fallback is a surface, so it steps with its substrate.

in a card

in a detail dialog

Installation

pnpm dlx shadcn@latest add https://matos-ui.com/r/cover-image.json

Usage

import { CoverImage } from '@/components/matos-ui/cover-image'
<CoverImage src={book.imageLinks?.thumbnail} alt={book.title} />

alt is required — it names the cover whether or not the image ever loads.

Never a broken frame

Four things end up in the same place: a missing src, a 404, a mixed-content block, and an image that decodes to nothing (naturalWidth === 0 even after load fires). None of them show the browser's broken-image icon — they all resolve to the fallback.

And the fallback is not a fixed bg-muted. It is Elevated offset={1} over whatever substrate the cover sits on. The same empty cover in a table cell, a card, and a detail dialog each reads as a surface at its own depth — §2.1, in a component every catalogue app needs.

Three states, two tiers

StateWhat showsTier
loading (no placeholder)An Elevated skeleton on a slow, ambient pulsereceding, easeInOut over three duration.slower beats
loading (with placeholder)The blurDataURL / colour, blurred, underneath
loadedThe image crossfades in with a blur→sharp focus pull (revealVariants)fast
empty / errorAn Elevated fallback with an icon

prefers-reduced-motion keeps the crossfade and drops the pulse and the blur.

Google Books and friends — sanitize

Google Books thumbnails arrive over http:// (blocked as mixed content on an https page) and with &edge=curl, which paints a folded-corner border; zoom=1 returns a postage stamp. sanitize forces https, drops edge=curl and bumps the zoom to 2.

<CoverImage src={dirtyUrl} alt={title} sanitize />

It is a pure exported function — use it on its own, and a URL that matches nothing comes back untouched, so it is safe to leave on for a whole list from a mixed source.

import { sanitizeCoverUrl } from '@/components/matos-ui/cover-image'

Generalises

A book cover, an avatar, a product thumbnail — swap ratio and radius. Every grubby API returns imageLinks half-missing.

<CoverImage src={user.avatarUrl} alt={user.name} ratio="square" radius="lg" />

Reference

CoverImage Props

PropTypeDefaultDescription
srcstring | nullThe image URL — may be missing, may be dirty.
altstringRequired. Names the cover whether it loads or not.
sanitizebooleanfalseFix http:// and Google Books borders before loading.
placeholderstringA blurDataURL or a CSS colour, shown blurred under the image.
fallbackReactNodeiconWhat fills the frame when there is no usable image.
ratio"book" | "portrait" | "square" | "video" | "wide""book"Frame aspect ratio.
radius"none" | "sm" | "md" | "lg""md"
fit"cover" | "contain""cover"
loading"lazy" | "eager""lazy"
onStatusChange(status) => void"empty" | "loading" | "loaded" | "error".

sanitizeCoverUrl

(url: string) => string — forces https, drops &edge=curl, swaps zoom=1 for zoom=2 on Google Books URLs. Idempotent and generic.