Componentdata
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-ratioOr install the full npm package:
npm install bigbulluiUsage
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>Source code
aspect-ratio.tsxZero 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
| Prop | Type | Description |
|---|---|---|
| ratio | number | "16:9" | "3:1" | "2:1" | "1:1" | "4:3" | "21:9" | Aspect ratio preset or custom numeric ratio (default '16:9'). |
| perforated | boolean | Renders dashed ticket perforation frame (default false). |
| cornerGuides | boolean | Renders corner punch registration guide marks (default false). |
| cornerNotches | boolean | Renders ticket punch notches on side edges (default false). |
| label | string | Micro status banner in the top-left corner. |
| serial | string | Secondary mono serial identifier. |
| variant | "default" | "perforated" | "stub" | "screen" | Visual frame surface styling (default 'default'). |