Componentdata1.77 KB gzipZero dependencies

Aspect Ratio

Proportional media container with preset ratios, framed border, and corner guides.

Preview

Live Preview
★ OFFICIAL TICKET STUB PROPORTION ★16:9 RATIOStage screen & cinema monitor

Installation

Tokens guide →

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

npx bigbullui add aspect-ratio

Or install the full npm package:

npm install bigbullui
import { AspectRatio } from "@/components/ui/aspect-ratio";

<AspectRatio ratio="16:9" perforated cornerGuides cornerNotches label="STAGE SCREEN">
  <div className="flex size-full items-center justify-center bg-secondary/40 font-mono text-xs text-muted-foreground">
    PROPORTIONAL STAGE FEED
  </div>
</AspectRatio>

Usage guidance

Use Aspect Ratio when a ticket stub surface needs Proportional media container with preset ratios, framed border, and corner guides. Keep props minimal and prefer composition with sibling stubs.

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

Source code

aspect-ratio.tsx

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

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

export type AspectRatioPreset = "16:9" | "3:1" | "2:1" | "1:1" | "4:3" | "21:9";

export const ASPECT_RATIO_PRESETS: Record<AspectRatioPreset, { ratio: number; label: string; description: string }> = {
  "16:9": { ratio: 16 / 9, label: "16:9 STAGE SCREEN", description: "Widescreen stage and cinema monitor" },
  "3:1": { ratio: 3 / 1, label: "3:1 BOARDING VOUCHER", description: "Elongated travel pass & wristband voucher" },
  "2:1": { ratio: 2 / 1, label: "2:1 ADMISSION PASS", description: "Classic arena ticket and concert stub" },
  "1:1": { ratio: 1 / 1, label: "1:1 BADGE", description: "Square accreditation pass and credential badge" },
  "4:3": { ratio: 4 / 3, label: "4:3 RETRO SCREEN", description: "Classic broadcast display" },
  "21:9": { ratio: 21 / 9, label: "21:9 ULTRA STAGE", description: "Panoramic arena backdrop display" },
};

export function isAspectRatioPreset(value: unknown): value is AspectRatioPreset {
  return typeof value === "string" && value in ASPECT_RATIO_PRESETS;
}

export interface AspectRatioProps extends React.HTMLAttributes<HTMLDivElement> {
  ratio?: number | AspectRatioPreset;
  perforated?: boolean;
  cornerGuides?: boolean;
  cornerNotches?: boolean;
  label?: string;
  serial?: string;
  variant?: "default" | "perforated" | "stub" | "screen";
  innerClassName?: string;
}

export function AspectRatio({
  ratio = "16:9",
  perforated = false,
  cornerGuides = false,
  cornerNotches = false,
  label,
  serial,
  variant = "default",
  className,
  innerClassName,
  style,
  children,
  ...props
}: AspectRatioProps) {
  const isPreset = isAspectRatioPreset(ratio);
  const preset = isPreset ? ASPECT_RATIO_PRESETS[ratio] : undefined;
  const resolvedRatio = typeof ratio === "number" ? ratio : preset?.ratio ?? 16 / 9;

  const displayLabel = label ?? preset?.label;
  const isPerforated = perforated || variant === "perforated";

  return (
    <div
      className={cn(
        "group relative w-full select-none overflow-hidden rounded-lg transition-all duration-200",
        // Surface variants
        variant === "default" && "border-2 border-foreground bg-card text-card-foreground shadow-sm",
        variant === "perforated" && "border-2 border-dashed border-foreground bg-card text-card-foreground shadow-sm",
        variant === "stub" && "border-2 border-foreground bg-card text-card-foreground shadow-[4px_4px_0_0_var(--color-border)]",
        variant === "screen" && "border-2 border-foreground bg-primary text-primary-foreground shadow-md",
        isPerforated && variant !== "perforated" && "border-dashed",
        className
      )}
      style={{
        aspectRatio: resolvedRatio,
        ...style,
      }}
      {...props}
    >
      {/* Inner Perforated Frame Guideline */}
      {isPerforated && (
        <div
          className="pointer-events-none absolute inset-1.5 z-10 rounded-md border border-dashed border-border/80"
          aria-hidden="true"
        />
      )}

      {/* Ticket Punch Cutout Notches (Left & Right) */}
      {cornerNotches && (
        <>
          <div
            className="pointer-events-none absolute left-0 top-1/2 z-20 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-foreground bg-background"
            aria-hidden="true"
          />
          <div
            className="pointer-events-none absolute right-0 top-1/2 z-20 size-4 translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-foreground bg-background"
            aria-hidden="true"
          />
        </>
      )}

      {/* Corner Punch Guides (L-brackets + Center Registration Target) */}
      {cornerGuides && (
        <>
          {/* Top-Left */}
          <div
            className="pointer-events-none absolute left-2 top-2 z-20 flex size-3 items-center justify-center border-l-2 border-t-2 border-foreground/60"
            aria-hidden="true"
          >
            <div className="size-0.5 rounded-full bg-foreground/70" />
          </div>

          {/* Top-Right */}
          <div
            className="pointer-events-none absolute right-2 top-2 z-20 flex size-3 items-center justify-center border-r-2 border-t-2 border-foreground/60"
            aria-hidden="true"
          >
            <div className="size-0.5 rounded-full bg-foreground/70" />
          </div>

          {/* Bottom-Left */}
          <div
            className="pointer-events-none absolute bottom-2 left-2 z-20 flex size-3 items-center justify-center border-b-2 border-l-2 border-foreground/60"
            aria-hidden="true"
          >
            <div className="size-0.5 rounded-full bg-foreground/70" />
          </div>

          {/* Bottom-Right */}
          <div
            className="pointer-events-none absolute bottom-2 right-2 z-20 flex size-3 items-center justify-center border-b-2 border-r-2 border-foreground/60"
            aria-hidden="true"
          >
            <div className="size-0.5 rounded-full bg-foreground/70" />
          </div>
        </>
      )}

      {/* Micro Info Label & Serial Banner */}
      {(displayLabel || serial) && (
        <div
          className="pointer-events-none absolute left-2.5 top-2.5 z-20 flex max-w-[calc(100%-20px)] items-center gap-1.5 truncate"
          aria-hidden="true"
        >
          {displayLabel && (
            <span className="truncate rounded border border-border/80 bg-background/90 px-1.5 py-0.5 font-mono text-[9px] font-bold uppercase tracking-widest text-foreground shadow-xs backdrop-blur-xs">
              {displayLabel}
            </span>
          )}
          {serial && (
            <span className="hidden truncate font-mono text-[9px] uppercase tracking-wider text-muted-foreground sm:inline-block">
              {serial}
            </span>
          )}
        </div>
      )}

      {/* Media & Content Slot */}
      <div
        className={cn(
          "absolute inset-0 size-full overflow-hidden [&>img]:size-full [&>img]:object-cover [&>video]:size-full [&>video]:object-cover",
          innerClassName
        )}
      >
        {children}
      </div>
    </div>
  );
}

Props

PropTypeDescription
ratio?number | AspectRatioPreset
perforated?boolean
cornerGuides?boolean
cornerNotches?boolean
label?string
serial?string
variant?"default" | "perforated" | "stub" | "screen"
innerClassName?string