Componentticket-stub0.78 KB gzipZero dependencies

Envelope

Envelope whose flap opens with a 3D rotateX and the letter inside rises, on hover or via an open prop.

Preview

Live Preview
CLOSEDFROM: Alice

You are invited.

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add envelope

Or install the full npm package:

npm install bigbullui
import { Envelope } from "@/components/ui/envelope";

<Envelope sender="ALICE" recipient="BOB" open={open} onOpenChange={setOpen}>
  <p>You are invited.</p>
</Envelope>

Usage guidance

Use Envelope when a ticket stub surface needs Envelope whose flap opens with a 3D rotateX and the letter inside rises, on hover or via an open prop. Keep props minimal and prefer composition with sibling stubs.

Avoid Envelope for long form flows or dense data grids where a dedicated layout block fits better.

Source code

envelope.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface EnvelopeProps {
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  sender?: string;
  recipient?: string;
  letter?: React.ReactNode;
}

export function Envelope({ open = false, onOpenChange, sender, recipient, letter }: EnvelopeProps) {
  return (
    <div
      className={cn(
        "relative size-32 rounded-2xl border border-border bg-card p-4 motion-reduce:transition-none",
        "hover:scale-[1.02] transition-transform"
      )}
      onClick={() => onOpenChange && onOpenChange(!open)}
    >
      <style>{`@keyframes envFlapOpen { from { transform: rotateX(-85deg); opacity: 0.4; } to { transform: rotateX(0deg); opacity: 1; } }`}</style>
      <div
        className={cn(
          "rounded-t-2xl border-b border-border bg-secondary p-3"
        )}
        style={{
          transformOrigin: "top",
          transform: open ? "rotateX(0deg)" : "rotateX(-12deg)",
          animation: open ? "envFlapOpen 0.45s ease-out both" : undefined,
        }}
      >
        <div className="flex items-center justify-between">
          <span className="font-mono text-[9px] uppercase tracking-[0.15em] text-muted-foreground">
            {open ? "OPEN" : "CLOSED"}
          </span>
          <span className="font-mono text-[9px] uppercase tracking-[0.15em] text-muted-foreground">
            {sender ? `FROM: ${sender}` : "SENDER"}
          </span>
        </div>
      </div>

      <div
        className="mt-3 p-3 bg-muted rounded-sm transition-transform duration-300 motion-reduce:transition-none"
        style={open ? { transform: "translateY(-8px)" } : undefined}
      >
        {letter}
      </div>

      <div className="absolute -bottom-1 w-full h-0.5 bg-border/20" />
    </div>
  );
}

Props

PropTypeDescription
open?boolean
onOpenChange?(open: boolean) => void
sender?string
recipient?string
letter?React.ReactNode