Componentmedia1.15 KB gzipZero dependencies

Gift Message

Gift message card with ribbon wiggle and open animation.

Preview

Live Preview

Installation

Tokens guide →

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

npx bigbullui add gift-message

Or install the full npm package:

npm install bigbullui
import { GiftMessage } from "@/components/ui/gift-message";

<GiftMessage />

Usage guidance

Use Gift Message when a ticket stub surface needs Gift message card with ribbon wiggle and open animation. Keep props minimal and prefer composition with sibling stubs.

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

Source code

gift-message.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 GiftMessageProps extends React.HTMLAttributes<HTMLDivElement> {
  from: string;
  message: string;
  onOpen?: () => void;
}

/** Gift message card: ribbon badge + envelope reveal animation. */
export function GiftMessage({ from, message, onOpen, className, ...props }: GiftMessageProps) {
  const [opened, setOpened] = React.useState(false);

  return (
    <div className={cn("w-full max-w-xs", className)} {...props}>
      <style>{`
        @keyframes gmPop { 0% { transform: scale(0.85) rotate(-3deg); opacity: 0; } 70% { transform: scale(1.05) rotate(1deg); } 100% { transform: scale(1) rotate(0); opacity: 1; } }
        @keyframes gmRibbon { 0%, 100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
      `}</style>

      {!opened ? (
        <button
          onClick={() => { setOpened(true); onOpen?.(); }}
          className="group relative w-full overflow-hidden rounded-lg border-2 border-dashed border-accent/60 bg-card p-6 text-center transition-transform duration-200 hover:scale-[1.02] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none"
          aria-label="Open gift"
        >
          <span className="mx-auto mb-3 block w-max animate-[gmRibbon_2s_ease-in-out_infinite] motion-reduce:animate-none" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-7"><rect x="4" y="9" width="16" height="3.6" rx="0.5" /><path d="M5.6 12.6v7.9h12.8v-7.9M12 9v11.5M12 9C8 9 6.5 7.8 6.5 6c0-1.2 1-2 2-2 1.8 0 2.6 2.6 3.5 5zM12 9c4 0 5.5-1.2 5.5-3 0-1.2-1-2-2-2-1.8 0-2.6 2.6-3.5 5z" /></svg></span>
          <span className="block font-mono text-[10px] uppercase tracking-[0.15em] text-muted-foreground">Gift from {from}</span>
          <span className="mt-1 block text-sm font-medium text-accent">Tap to open</span>
        </button>
      ) : (
        <div className="rounded-lg border border-accent/50 bg-gradient-to-br from-accent/10 to-card p-5 animate-[gmPop_0.4s_cubic-bezier(0.34,1.56,0.64,1)_both] motion-reduce:animate-none">
          <p className="font-mono text-[10px] uppercase tracking-[0.15em] text-muted-foreground">From: {from}</p>
          <p className="mt-2 text-sm leading-relaxed">{message}</p>
        </div>
      )}
    </div>
  );
}

Props

PropTypeDescription
fromstring
messagestring
onOpen() => void