Skip to content

Frame Card

A photo card whose sharp photo shrinks inside a blurred copy of itself on hover, while the title rolls up.

$npx shadcn@latest add https://framebrick.co/r/frame-card.json

From the Tustel template, see it in context.

Installation

$ npx shadcn@latest add https://framebrick.co/r/frame-card.json

Usage

import { FrameCard } from "@/components/ui/frame-card";

<FrameCard
  href="/work/aurora"
  title="Aurora Coffee"
  meta="Brand photography"
  ratio="3 / 4"
  rollClassName="text-orange-600"
  media={<Image src={cover} alt="" fill sizes="(min-width: 1024px) 33vw, 100vw" />}
/>

Props

PropTypeDefaultDescription
mediaReactNode—The photo. It is drawn twice — sharp, and blurred behind it as the frame — so pass an image, with an empty alt text.
titlestring—Shown twice: the second copy rolls into view on hover.
metaReactNode—A small line at the right of the title row, e.g. the category.
hrefstring—Makes the whole card a Next.js link; a full address opens in a new tab. Without it the card is a plain box.
ratiostring"4 / 3"Proportions of the photo box, as a CSS aspect-ratio value: "4 / 3", "367 / 550".
shrinknumber0.85How wide the sharp photo is on hover, as a share of the card.
rollClassNamestring—Classes for the second copy of the title — a colour, say. The library ships no accent.
classNamestring—Classes for the card.
  • Plain CSS: no animation library and no JavaScript. Framer's spring (bounce 0, 0.5 s) is approximated by cubic-bezier(0.22, 1, 0.36, 1) over 500 ms; with reduced motion the hover state simply changes. Keyboard focus shows the same state as hover.
  • The blurred copy is scaled up 4% so the blur does not fade out at the edge of the box, which Tustel's does. The photo has the library's rounded corners.
  • The link is named by the title and the meta: the photos are decorative, the blurred copy is aria-hidden, and so are the two rolling copies of the title — an invisible first copy carries the name.
  • The title stays on one line; in a narrow card the small text drops under it, right-aligned, instead of squeezing it.
  • Tustel's second title is orange; the library has no accent colour, so `rollClassName` takes it.
  • For tests: the link's textContent holds the title three times (the invisible copy and the two that roll). Query by role and name — it reads once — or check the visible text.
  • Give each card its own `ratio` to mix portrait and landscape in one grid, as Tustel does: top-align the cards (`items-start`) and each is as tall as its photo.

AI prompt

Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.

Create a React + Tailwind CSS component called FrameCard — plain CSS, no animation library, no JavaScript.
- The root is a Next.js Link (an <a> that opens in a new tab for a full address, a div without href) with the group class:
  flex column, gap-2.5, rounded-lg, a focus ring ring-[3px] ring-ring/50. A CSS variable --frame-shrink holds the hover
  width (85%).
- A photo box: relative, w-full, overflow-hidden, rounded-lg, bg-muted, aspect-ratio from the ratio prop. Inside it the
  photo twice: first a blurred copy (absolute inset-0, blur-[3px], scale-[1.04], aria-hidden), then the sharp one
  (absolute, centred with top-1/2 left-1/2 and -translate-x-1/2 -translate-y-1/2, w-full, the same aspect-ratio,
  rounded-lg, overflow-hidden). The children of both are stretched with size-full and object-cover.
- On group-hover and group-focus-visible the sharp photo's width goes to var(--frame-shrink): transition-[width] over
  500ms with cubic-bezier(0.22,1,0.36,1), motion-reduce:transition-none. The blurred copy shows around it as a frame.
- Under the photo a row (flex flex-wrap, items-end, justify-between, gap-x-2.5): the title (text-base font-medium) and the
  meta (text-sm text-muted-foreground, shrink-0, ml-auto), which drops under the title when the card is too narrow for both.
- The title rolls up: a relative, overflow-hidden, whitespace-nowrap box with an invisible copy (opacity-0 — it sizes the box
  and names the link) and an absolutely positioned column of two copies, each with pb-2.5 and aria-hidden; the second takes rollClassName.
  On group-hover and group-focus-visible the column moves -50% on Y (transition-transform, the same 500ms curve).

Be the first to know

Get new templates and components first

One short email when we release a template or a free component. No spam — unsubscribe anytime.

We use your email only for Framebrick updates. Privacy policy