Studio KitHigh-End Animated Cardsmotion/react

RevealCard

Card whose accent-colored overlay springs in from any edge on hover or keyboard focus to reveal the title, body copy and optional actions.

Preview

Reveal Card

A spring-driven overlay slides in on hover or keyboard focus, carrying the detail without leaving the card.

Customize

4 props

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add reveal-card

Writes components/ui/reveal-card.tsx and components/styles/reveal-card.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { RevealCard } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <RevealCard />6  );7}

Props

PropTypeDefault
title

Title shown on the cover and the overlay.

string'Reveal Card'
body

Body copy revealed in the overlay.

string'A spring-driven overlay slides in on hover or keyboard focus, carrying the detail without leaving the card.'
revealFrom

Edge the overlay slides in from.

'bottom' | 'top' | 'left' | 'right''bottom'
accentColor

Overlay background color.

string'#38bdf8'
children

Extra overlay content such as a link or button.

React.ReactNodeundefined
className

Extra class merged onto the root.

string''

Accessibility

Root is a focusable <article> labelled by the title; the overlay opens on hover and on focus-within, so tabbing to the card or to a link inside reveals it. The decorative cover is aria-hidden while the overlay text stays in the accessibility tree, and prefers-reduced-motion replaces the slide with a short fade.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/reveal-card.tsx
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/reveal-card.module.css";6 7export interface RevealCardProps {8  /** Card title, shown on the cover and the overlay */9  title?: string;10  /** Body copy revealed in the overlay */11  body?: string;12  /** Edge the overlay slides in from */13  revealFrom?: "bottom" | "top" | "left" | "right";14  /** Overlay background color */15  accentColor?: string;16  /** Extra overlay content (e.g. a link or button) rendered under the body */17  children?: React.ReactNode;18  /** Optional className */19  className?: string;20}21 22const HIDDEN: Record<NonNullable<RevealCardProps["revealFrom"]>, { x: string; y: string }> = {23  bottom: { x: "0%", y: "100%" },24  top: { x: "0%", y: "-100%" },25  left: { x: "-100%", y: "0%" },26  right: { x: "100%", y: "0%" },27};28 29export function RevealCard({30  title = "Reveal Card",31  body = "A spring-driven overlay slides in on hover or keyboard focus, carrying the detail without leaving the card.",32  revealFrom = "bottom",33  accentColor = "#38bdf8",34  children,35  className = "",36}: RevealCardProps) {37  const prefersReducedMotion = useReducedMotion();38  const [hovered, setHovered] = React.useState(false);39  const [focused, setFocused] = React.useState(false);40  const open = hovered || focused;41  const hidden = HIDDEN[revealFrom] ?? HIDDEN.bottom;42 43  return (44    <article45      tabIndex={0}46      aria-label={title}47      onPointerEnter={() => setHovered(true)}48      onPointerLeave={() => setHovered(false)}49      onFocus={() => setFocused(true)}50      onBlur={(e) => {51        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocused(false);52      }}53      data-open={open || undefined}54      style={{ "--mf-rc-accent": accentColor } as React.CSSProperties}55      className={`${styles.root} ${className}`.trim()}56    >57      <div className={styles.cover} aria-hidden="true">58        <span className={styles.glyph}>59          <svg viewBox="0 0 24 24" width="20" height="20">60            <path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />61          </svg>62        </span>63        <span className={styles.coverTitle}>{title}</span>64      </div>65      <motion.div66        className={styles.overlay}67        initial={false}68        animate={69          prefersReducedMotion70            ? { x: "0%", y: "0%", opacity: open ? 1 : 0 }71            : open72              ? { x: "0%", y: "0%", opacity: 1 }73              : { ...hidden, opacity: 1 }74        }75        transition={76          prefersReducedMotion ? { duration: 0.15 } : { type: "spring", stiffness: 300, damping: 30 }77        }78      >79        <h3 className={styles.title}>{title}</h3>80        <p className={styles.body}>{body}</p>81        {children ? <div className={styles.extra}>{children}</div> : null}82      </motion.div>83    </article>84  );85}