npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

card-morph

v0.1.2

Published

Morphing card transition component for React.

Readme

card-morph

card-morph is a React component for smooth card-to-detail transitions.

card-morph demo

Features

  • Card morph handoff measured in DOM space for pixel-accurate close alignment
  • Automatic close-target stabilization for transformed, blurred, or faded parent shells
  • Open and close easing with tuned defaults
  • Reusable geometry hooks (measureClosedGeometry, getExpandedGeometry, writeGeometry)
  • Protected motion root, so custom content transforms do not overwrite the card morph
  • Keyboard accessibility (Enter + Space to open, Escape to close)
  • Body-scroll lock with scroll-position preservation and phase callbacks
  • Framework-neutral styling contract (your CSS, your theme)

Install

npm install card-morph

or

yarn add card-morph

Package exports

import { CardMorph } from "card-morph";

Quick setup

Render CardMorph where a card should expand into a detail view. You provide the closed card, the expanded overlay, and three geometry callbacks. The component handles DOM measurement, easing, keyboard open, Escape-to-close, body scroll locking, and the final handoff back to the real card.

import { CardMorph, type CardMorphBox } from "card-morph";

type StoryGeometry = CardMorphBox & {
  heroHeight: number;
  titleFontSize: number;
};

const mix = (from: number, to: number, progress: number) => from + (to - from) * progress;

function readClosedGeometry(card: HTMLElement, rect: CardMorphBox): StoryGeometry {
  const title = card.querySelector<HTMLElement>(".story-card__title");
  const titleFontSize = title ? Number.parseFloat(getComputedStyle(title).fontSize) : 28;

  return {
    ...rect,
    heroHeight: rect.height,
    titleFontSize
  };
}

export function StoryCard() {
  return (
    <CardMorph<StoryGeometry>
      ariaLabel="Open story"
      className="story-card"
      getExpandedGeometry={(box) => ({
        ...box,
        heroHeight: Math.min(window.innerHeight * 0.64, 720),
        titleFontSize: 44
      })}
      measureClosedGeometry={readClosedGeometry}
      writeGeometry={(element, from, to, progress) => {
        element.style.setProperty("--story-hero-height", `${mix(from.heroHeight, to.heroHeight, progress)}px`);
        element.style.setProperty(
          "--story-title-font-size",
          `${mix(from.titleFontSize, to.titleFontSize, progress)}px`
        );
      }}
      renderClosed={() => <CardContent expanded={false} />}
      renderOverlay={({ close, phase }) => (
        <CardContent expanded onClose={close} phase={phase} />
      )}
    />
  );
}

Use CSS variables from writeGeometry in your card styles:

.story-card__hero {
  height: var(--story-hero-height);
}

.story-card__title {
  font-size: var(--story-title-font-size);
}

Your geometry type is yours. Start with CardMorphBox (x, y, width, height) and add the values your content needs to interpolate: image height, title size, padding, opacity, or anything else that should move with the morph.

writeGeometry receives an inner content wrapper, not the moving overlay shell. Use it for CSS variables, opacity, transforms, and content-level interpolation without overwriting the root card motion.

API

Geometry Callbacks

These callbacks define the morph. They all share your generic geometry type, so the values you measure in the closed card are the same values you write during the animation.

| Callback | Description | | --- | --- | | measureClosedGeometry(card, rect) | Reads the resting card. rect is the measured CardMorphBox; return it with any extra content values you want to animate. | | getExpandedGeometry(box) | Returns the matching geometry for the expanded state. box is full viewport by default, or the value returned by getExpandedBox. | | writeGeometry(element, from, to, progress) | Runs during motion with progress from 0 to 1. Interpolate from to to and write CSS variables or content transforms to element. |

Render State

Both renderClosed and renderOverlay receive:

type CardMorphRenderState = {
  close: () => void;
  isExpanded: boolean;
  open: () => void;
  phase: CardMorphPhase;
};

Use open or close for custom buttons, isExpanded to switch content, and phase for timed reveals.

Phases

type CardMorphPhase = "closed" | "opening" | "open" | "closing" | "settled";

| Phase | Meaning | | --- | --- | | closed | The overlay is not mounted; the real card is interactive. | | opening | The overlay is morphing from the measured card to the expanded box. | | open | The overlay is fully expanded and responsive to resize. | | closing | The overlay is morphing back to the latest measured card position. | | settled | The final close handoff frame before the overlay unmounts. |

Props

| Prop | Type | Description | | --- | --- | --- | | ariaLabel | string | Accessible label for the pressable closed card. | | className | string | Base class applied to the closed card and overlay. | | renderClosed | (state) => ReactNode | Renders the resting card content. | | renderOverlay | (state) => ReactNode | Renders the expanded and closing overlay content. | | measureClosedGeometry | (card, rect) => TGeometry | Reads the closed card geometry. | | getExpandedGeometry | (box) => TGeometry | Describes the expanded content geometry. | | writeGeometry | (element, from, to, progress) => void | Writes interpolated content geometry each frame. | | getExpandedBox | () => CardMorphBox | Overrides the expanded bounds. Defaults to the viewport. | | getMeasurementRoot | (card) => HTMLElement \| null | Advanced override for the element temporarily neutralized before close measurement. The package automatically handles the common transformed-parent case. | | onPhaseChange | (phase) => void | Fires when the component enters a new phase. | | lockBodyScroll | boolean | Locks body scroll while the overlay is mounted. Defaults to true. | | openDuration | number | Open duration in milliseconds. Defaults to 380. | | closeDuration | number | Close duration in milliseconds. Defaults to 460. | | openEase | (progress) => number | Custom easing for the open animation. | | closeEase | (progress) => number | Custom easing for the close animation. | | openRadius | number | Border radius at the expanded state. Defaults to 0. | | closedRadius | number | Border radius at the closed state. Defaults to 29. | | pressableClassName | string | Class added to the interactive closed card. Defaults to is-pressable. | | ghostClassName | string | Class added to the hidden closed card while the overlay is mounted. Defaults to is-hidden. | | overlayClassName | string | Class added to the fixed overlay. Defaults to is-overlay. | | expandedClassName | string | Class added while expanded content is active. Defaults to is-expanded. | | closingClassName | string | Class added during the close animation. Defaults to is-closing. | | settledClassName | string | Class added for the final close handoff frame. Defaults to is-settled. |

Repo layout

  • src/components/card-morph/ — reusable component source
  • src/index.ts — package entrypoint
  • vite.lib.config.ts — package build entry
  • tsconfig.lib.json — declaration output settings
  • src/App.tsx — demo app for integration testing

Demo development

  • npm run dev starts local preview on 127.0.0.1
  • npm run build builds the demo and package bundle/types

The npm package does not include demo images. Demo-only artwork lives in public/assets/ for the GitHub repo and local preview.

License and Use

card-morph is released under the MIT License. The package is provided as-is, without warranty or liability for production use. The demo artwork in this repo is original and included for preview purposes; bring your own visuals for your app and test the interaction in your own layout before shipping.