bottega

cards

Book Flip Card

A 3D book open/close card: a base card with a cover panel that rotates open on hover via CSS perspective + rotateY(-80deg) (~0.5s ease). The transform-origin sits on the spine (left edge) so it mimics opening a book. Inner content is revealed beneath. CSS-only, no JS — works as a React Server Component. Cover maps to --primary / --primary-foreground; base to --background / --foreground.

Ported from eslam-hany (MIT)

Hello


Install

1. Register the namespace (once per project):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/book-flip-card

Usage

Usagetsx
<BookFlipCard />

Props

PropTypeDefaultDescription
coverLabelstringHover MeText shown on the closed cover
innerContentstringHelloText shown inside once the cover opens

Source

book-flip-card.tsxtsx
import styles from "./book-flip-card.module.css";

export interface BookFlipCardProps {
  /** Text shown on the closed cover */
  coverLabel?: string;
  /** Text shown inside once the cover opens */
  innerContent?: string;
}

/**
 * A 3D book open/close card. The cover panel rotates open on hover (CSS-only,
 * `perspective` + `rotateY(-80deg)`, spine at transform-origin: 0) revealing the
 * inner content beneath it. No JS — pure CSS transition.
 *
 * Port of: https://uiverse.io/eslam-hany/selfish-bobcat-73 (MIT, eslam-hany)
 */
export function BookFlipCard({
  coverLabel = "Hover Me",
  innerContent = "Hello",
}: BookFlipCardProps) {
  return (
    // tabIndex+role make the card keyboard-openable: focus triggers the same
    // `.book:focus-within .cover` flip as hover. aria-label surfaces the label
    // (the cover is aria-hidden) + the revealed content to screen readers.
    <div
      className={styles.book}
      tabIndex={0}
      role="group"
      aria-label={`${coverLabel}: ${innerContent}`}
    >
      <p className={styles.innerText}>{innerContent}</p>
      <div className={styles.cover} aria-hidden="true">
        <p className={styles.coverText}>{coverLabel}</p>
      </div>
    </div>
  );
}

Dependencies

  • @bottega/tokens