Skip to content

Inset Card

A card whose photo sits in a padded frame and fills it on hover, closing the gap to its detail box.

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

From the Balancee template, see it in context.

Examples

Single surface

One card: the photo is padded inside it and fills the top of it on hover; the text does not move.

<InsetCard join={false} href="/journal/studio-notes" media={<Image src={photo} alt="" fill />}>…</InsetCard>

Installation

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

Usage

import { InsetCard } from "@/components/ui/inset-card";

<InsetCard href="/services/strategy" media={<Image src={photo} alt="" fill sizes="25vw" />}>
  <span className="font-medium">Business strategy</span>
  <span className="text-sm text-foreground/75">Positioning and a plan your team can follow.</span>
</InsetCard>

Props

PropTypeDefaultDescription
mediaReactNode—The photo: an image, with an empty alt text. It is stretched to the frame and clipped.
childrenReactNode—The detail under the photo: a title and a line, say.
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"3 / 2"Proportions of the photo frame, as a CSS aspect-ratio value.
insetnumber5Padding of the frame at rest, in px. When joined it is also the gap to the detail box.
joinbooleantrueTwo boxes that close into one on hover; false is one surface with the photo padded inside it.
classNamestring—Classes for the card (its width, say).
  • It is Balancee's Service, Post and Team cards: the photo sits in a frame with 5 px of padding (photo corners 7 px, frame 10), and on hover the padding goes to 0 so the photo grows to the edge (357 to 367 px wide, 5 px up and left) and its corners square off. Joined (Service, Team), the frame and the detail box are two boxes 5 px apart; on hover the gap closes and the frame's bottom corners and the detail box's top corners go from 10 to 0, so the two become one card. Single surface (Post) keeps one card and only the photo moves.
  • Plain CSS: no animation library and no JavaScript, so it works as it is on a server page. Framer's spring (bounce 0, 0.4 s) is a CSS linear() curve sampled from the spring itself, within 1% of it, over 450 ms, as Balancee's own port runs it; a browser without linear() gets cubic-bezier(0.22, 1, 0.36, 1).
  • Padding, gap and radius are not transform or size keys, so <MotionConfig> would not skip them: with reduced motion the state simply changes (motion-reduce:transition-none). Hover only applies where the device can hover; keyboard focus shows the same state.
  • Secondary text on the muted surface needs its own tone: text-muted-foreground on bg-muted is 4.3:1 in the default theme, so the demos use text-foreground/75. The photo is decoration (alt=""): the link is named by the detail text. The look is the library's (bg-muted, rounded-xl, rounded-md); the 5 px is Balancee's, a number.
  • It is not Zoom Card (which scales its content inside a fixed frame) or Frame Card (which shrinks the sharp photo inside a blurred copy: the other direction).

AI prompt

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

Create a React + Tailwind CSS component called InsetCard — plain CSS, no animation library, no JavaScript, so it works in a server component.
- Props: media (the photo), children (the detail), href, ratio ("3 / 2"), inset (px, 5), join (true), className. Set --inset inline. 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, rounded-xl, a focus ring ring-[3px] ring-ring/50.
- join: the root has gap-(--inset) and goes to gap-0 on hover and focus-visible (transition-[gap]). A frame span (overflow-hidden, rounded-xl, bg-muted, p-(--inset), the aspect-ratio from the prop) holds a photo span (size-full, overflow-hidden, rounded-md, children stretched with size-full object-cover). Then a detail span: flex-1, flex column, rounded-xl, bg-muted, px-5 py-4. On group-hover and group-focus-visible the frame's padding goes to 0 and its bottom corners to 0 (rounded-b-none), the photo's corners to 0 and the detail's top corners to 0 (rounded-t-none).
- Without join: the root is one surface (overflow-hidden, bg-muted); the frame has no background or radius of its own, only the padding; the detail has no background and sits under it. The photo's padding and corners change on hover the same way.
- Every transition lasts 450ms on a CSS linear() list sampled from a spring with bounce 0 and duration 0.4s (about 30 stops) behind supports-[transition-timing-function:linear(0,1)]:, with cubic-bezier(0.22,1,0.36,1) as the fallback; motion-reduce:transition-none.

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