docs / patterns / mockup-showcase
MockupShowcase
browser or device chrome around whatever you put inside: a next/image, a bare img, a video or a live iframe. the frame is the value.
intensity · structuresrsc · server
provenance
shaped from personal-websitelayer
core / patternsintensity
structures · organizes a region, stays out of the contentcomposition
the visual slot of HeroSplit, a case study, a feature sectionchildren keep their own alt text and semanticsnot a window manager. nothing inside the chrome is interactivenever nested in another MockupShowcasea11y
the chrome, dots and address bar are allaria-hidden · the media carries its own altjest-axelive demo · try it out
your screenshot here
customize
framewhich chrome to draw
aspectaspect-ratio of the media well
urladdress bar text, browser frame only
usage
import { MockupShowcase } from "usva/patterns/mockup-showcase";
<MockupShowcase url="usva.dev">
<Image src={shot} alt="The usva docs homepage" />
</MockupShowcase>props
| prop | type | default | notes |
|---|---|---|---|
| frame | "browser" | "device" | "none" | "browser" | which chrome to draw. |
| url | string | — | decorative text in the browser address bar. |
| aspect | string | "16/10" | any CSS aspect-ratio value. |
| children | ReactNode | — | the media. stretched to fill the well, whatever element it is. |
get it
npx shadcn add https://usva.build/r/mockup-showcase.jsonsource · components/ui/mockup-showcase.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
export type MockupFrame = "browser" | "device" | "none";
export interface MockupShowcaseProps
extends React.HTMLAttributes<HTMLDivElement> {
frame?: MockupFrame;
/** Decorative text in the browser chrome's address bar. */
url?: string;
/** Any CSS aspect-ratio value, or `"auto"` to let live children set the
* height instead of locking a ratio and cropping them to cover. */
aspect?: string;
}
/**
* Device and browser chrome around arbitrary media.
*
* There is no image abstraction on purpose: children may be a next/image, a
* bare img, a video, or a live iframe. The well stretches whatever it gets, so
* an unknown child element still fills the frame.
*/
export const MockupShowcase = React.forwardRef<
HTMLDivElement,
MockupShowcaseProps
>(
(
{ className, frame = "browser", url, aspect = "16/10", children, ...props },
ref,
) => {
const fluidHeight = aspect === "auto";
return (
<div
ref={ref}
className={cn(
"overflow-hidden border border-border bg-surface shadow-floating",
frame === "device" ? "rounded-[1.75rem] p-2" : "rounded-xl",
className,
)}
{...props}
>
{frame === "browser" && (
<div
aria-hidden="true"
className="flex items-center gap-2 border-b border-border bg-surface-2 px-3 py-2.5"
>
<span className="flex gap-1.5">
<span className="size-2.5 rounded-full bg-border-strong" />
<span className="size-2.5 rounded-full bg-border-strong" />
<span className="size-2.5 rounded-full bg-border-strong" />
</span>
{url != null && (
<span className="ml-2 flex-1 truncate rounded-md bg-sunken px-2.5 py-1 font-mono text-[0.6875rem] text-muted">
{url}
</span>
)}
</div>
)}
{frame === "device" && (
<div
aria-hidden="true"
className="mx-auto mb-2 h-1.5 w-24 rounded-full bg-border-strong"
/>
)}
<div
data-testid="mockup-well"
style={fluidHeight ? undefined : { aspectRatio: aspect }}
className={cn(
"relative w-full overflow-hidden bg-sunken",
fluidHeight
? "[&>*]:w-full"
: "[&>*]:h-full [&>*]:w-full [&>*]:object-cover",
frame === "device" && "rounded-[1.25rem]",
)}
>
{children}
</div>
</div>
);
},
);
MockupShowcase.displayName = "MockupShowcase";