Media Box
A measured media frame for images, video posters, and player overlays.
MediaBox is a reusable media frame. It measures the available column width and sizes
a frame by kind: an image takes the media's natural aspect ratio (the cover
fills it with no crop and no letterbox); a video snaps to a standard 16:9
(landscape) / 9:16 (portrait) frame by orientation and cover-crops the poster to it.
It clips children to rounded corners and leaves playback controls to the caller through
children.
It composes CardMedia: CardMedia owns the object-cover
cover image plus the load retry-on-error and fallback, while MediaBox's job is to
measure the media, pick the frame ratio, and bound the frame width by
minWidth / maxWidth / maxHeight.
It does not own a video player. Compose an Artplayer, iframe, or custom
controls in the overlay slot when the consuming app needs playback; a <video>
child is clipped to the frame's rounded corners.
Installation
npx shadcn add https://acrylic-ui.vercel.app/r/media-box.jsonUsage
"use client"import * as React from "react"import { Maximize2, Play } from "lucide-react"import { Badge } from "@/registry/acrylic/badge"import { Button } from "@/registry/acrylic/button"import { MediaBox, type MediaBoxSizingSnapshot,} from "@/registry/acrylic/media-box"import { ExampleBackdrop } from "@/components/example-backdrop"const items = { wide: { label: "Wide video", kind: "video" as const, src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?q=80&w=1200&auto=format&fit=crop", naturalWidth: 1600, naturalHeight: 900, }, portrait: { label: "Tall image", kind: "image" as const, src: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=720&auto=format&fit=crop", naturalWidth: 720, naturalHeight: 960, }, player: { label: "Overlay slot", kind: "video" as const, src: "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?q=80&w=1200&auto=format&fit=crop", naturalWidth: 1280, naturalHeight: 720, },}type Key = keyof typeof itemsconst MIN_WIDTH = 240const MAX_WIDTH = 520export default function MediaBoxDemo() { const [active, setActive] = React.useState<Key>("wide") const [snapshot, setSnapshot] = React.useState<MediaBoxSizingSnapshot | null>(null) const item = items[active] return ( <ExampleBackdrop className="flex-col gap-4"> <div className="flex gap-2"> {Object.entries(items).map(([key, value]) => ( <Button key={key} size="small" variant={active === key ? "default" : "neutral"} onClick={() => setActive(key as Key)} > {value.label} </Button> ))} </div> <div className="w-full max-w-[520px]"> <MediaBox kind={item.kind} src={item.src} alt="" naturalWidth={item.naturalWidth} naturalHeight={item.naturalHeight} minWidth={MIN_WIDTH} maxWidth={MAX_WIDTH} frameClassName="mx-auto" onSizingChange={setSnapshot} > {active === "player" ? ( <div className="absolute inset-0 flex items-center justify-center bg-black/15"> <span className="flex size-12 items-center justify-center rounded-full bg-black/55 text-white shadow-sm"> <Play className="size-5 translate-x-px fill-current" /> </span> <Button icon size="small" variant="ghost" aria-label="Open media" className="absolute right-2 top-2 bg-black/45 text-white hover:bg-black/60" > <Maximize2 /> </Button> </div> ) : null} </MediaBox> {/* Constraints → result: the width bounds read as compact pills (the inputs), the live measured size is the emphasized output. */} <div className="mt-2 flex items-center justify-between gap-3 rounded-lg bg-[var(--acr-card-nested)] px-3 py-2"> <div className="flex items-center gap-1.5"> <Badge variant="secondary" size="sm" className="tabular-nums">min {MIN_WIDTH}</Badge> <Badge variant="secondary" size="sm" className="tabular-nums">max {MAX_WIDTH}</Badge> </div> <span className="text-[12px] tabular-nums text-foreground"> {snapshot ? ( <> {snapshot.box.width} × {snapshot.box.height} <span className="ml-1 text-muted-foreground">px</span> </> ) : ( <span className="text-muted-foreground">measuring…</span> )} </span> </div> </div> </ExampleBackdrop> )}import { MediaBox } from "@/components/acrylic/media-box"
<MediaBox
kind="video"
src={poster}
naturalWidth={1280}
naturalHeight={720}
maxWidth={520}
>
<div className="absolute inset-0">{/* player overlay */}</div>
</MediaBox>Props
| prop | type | default | description |
|---|---|---|---|
kind | "image" | "video" | "image" | selects the default maxHeight |
src | string | — | image URL or video poster URL |
naturalWidth / naturalHeight | number | — | known intrinsic poster/image size; skips measurement (no reflow) |
mediaSize | { width: number; height: number } | null | — | actual media size, preferred over poster size |
minWidth | number | — | hard floor on frame width — wins over maxWidth / maxHeight on conflict |
maxWidth | number | — | cap the frame width |
maxHeight | number | image: 510, video: 507 | cap frame height (reduces width to honor it) |
videoMaxHeight | number | — | extra height ceiling applied only to kind="video" (clamped with maxHeight) — keep tall images while capping vertical videos to a compact height, still at natural ratio (no letterbox) |
frameClassName | string | — | classes for the rounded frame |
imageClassName | string | — | classes for the poster/image |
fallback | ReactNode | image-off icon | custom broken-image fallback |
onSizingChange | (snapshot) => void | — | reports measured sizing details |
maxRetries | number | 2 | failed loads to retry (exponential backoff) before showing fallback |
retryDelayMs | number | 800 | base backoff delay in ms; doubles each attempt |
Plus native <div> props on the wrapper.