# Card Hover Expand

> A card row where the hovered card grows wider, its photo fades in and the title drops to the bottom over a gradient. Every card stays expanded on small screens. Pure CSS.

A shadcn/ui block from the [@tween-ui](https://tween-ui.vercel.app) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/tween-ui/card-hover-expand).

- Page, with a live preview: https://shadcn.exchange/r/tween-ui/card-hover-expand
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/tween-ui/card-hover-expand-light-c17d6889d427f6f7.png
- Registry item: https://tween-ui.vercel.app/r/card-hover-expand.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://tween-ui.vercel.app/r/card-hover-expand.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Notes from the registry

Docs, props and examples: https://tween-ui.vercel.app/block/card-hover-expand
The sample content loads images from images.unsplash.com. Replace them with your own before shipping.

## Files

### `components/tweenui/card-hover-expand.tsx`

```tsx
'use client';

import { useState, type ComponentPropsWithoutRef } from 'react';
import { cn } from '@/lib/utils';

export interface CardHoverExpandItem {
  /** Large index shown in the corner, e.g. ".01". */
  step: string;
  /** Small eyebrow above the title. */
  label: string;
  /** Card heading. */
  title: string;
  /** Photo revealed on the expanded card. */
  image: string;
  /** Alt text for the photo. */
  imageAlt: string;
}

export interface CardHoverExpandProps extends ComponentPropsWithoutRef<'div'> {
  /** Cards in the row. Defaults to a 3-card sample. */
  cards?: CardHoverExpandItem[];
}

const DEFAULT_CARDS: CardHoverExpandItem[] = [
  {
    step: '.01',
    label: 'Motion first',
    title: 'Hover systems that feel native',
    image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=800&h=1000&fit=crop',
    imageAlt: 'Team reviewing hover interactions on a whiteboard',
  },
  {
    step: '.02',
    label: 'Copy and own',
    title: 'Drop in the source, keep the motion',
    image: 'https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=800&h=1000&fit=crop',
    imageAlt: 'Developer working with component source in an editor',
  },
  {
    step: '.03',
    label: 'Ship ready',
    title: 'Reduced-motion built in from the start',
    image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=1000&fit=crop',
    imageAlt: 'Team shipping a product together',
  },
];

export default function CardHoverExpand({
  cards = DEFAULT_CARDS,
  className,
  ...props
}: CardHoverExpandProps) {
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <div
      data-card-hover-expand
      data-active-card={activeIndex}
      className={cn(
        'grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:flex lg:items-stretch lg:justify-center',
        className
      )}
      {...props}
    >
      {cards.map((card, index) => {
        const active = activeIndex === index;

        return (
          <article
            key={card.step + card.title}
            data-expand-card
            data-active={active ? 'true' : undefined}
            tabIndex={0}
            onMouseEnter={() => setActiveIndex(index)}
            onFocus={() => setActiveIndex(index)}
            className={cn(
              'relative h-[500px] w-full overflow-hidden rounded-2xl border border-[#045f64]/20 bg-white p-6 transition-all duration-700 ease-out outline-none motion-reduce:transition-none',
              'focus-visible:ring-2 focus-visible:ring-[#045f64] dark:border-[#045f64]/40 dark:bg-[#12161F] dark:ring-offset-[#12161F]',
              'lg:w-[28%]',
              active && 'lg:w-[40%]'
            )}
          >
            <figure
              className={cn(
                'absolute inset-0 size-full opacity-100 transition-opacity duration-700 ease-out motion-reduce:transition-none lg:opacity-0',
                active && 'lg:opacity-100'
              )}
            >
              <img src={card.image} alt={card.imageAlt} className="size-full object-cover" />
            </figure>
            <div
              className={cn(
                'absolute inset-0 bg-linear-to-t from-black to-transparent to-55% opacity-100 transition-opacity duration-700 ease-out motion-reduce:transition-none lg:opacity-0',
                active && 'lg:opacity-100'
              )}
            />
            <div
              className={cn(
                'absolute top-6 left-6 z-30 max-w-[15rem] translate-y-[calc(500px-100%-5rem)] space-y-3 transition-transform duration-700 ease-out motion-reduce:transition-none lg:translate-y-0',
                active && 'lg:translate-y-[calc(500px-100%-5rem)]'
              )}
            >
              <p
                className={cn(
                  'text-sm text-[#c6f56f] transition-colors duration-500 motion-reduce:transition-none lg:text-[#045f64] dark:lg:text-[#9fd4d6]',
                  active && 'lg:text-[#c6f56f] dark:lg:text-[#c6f56f]'
                )}
              >
                {card.label}
              </p>
              <h3
                className={cn(
                  'text-lg font-medium text-white transition-colors duration-500 motion-reduce:transition-none md:text-xl lg:text-[#12161F] dark:lg:text-white',
                  active && 'lg:text-white dark:lg:text-white'
                )}
              >
                {card.title}
              </h3>
            </div>
            <p
              className={cn(
                'absolute right-6 bottom-6 z-30 text-4xl font-medium text-white transition-colors duration-500 motion-reduce:transition-none md:text-5xl lg:text-[#12161F] dark:lg:text-white',
                active && 'lg:text-white dark:lg:text-white'
              )}
            >
              {card.step}
            </p>
          </article>
        );
      })}
    </div>
  );
}
```
