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
Customize
4 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add reveal-cardWrites 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.
1import { RevealCard } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <RevealCard />6 );7}Props
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
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}