Skip to content
← components

Compact Cartridge

new

A 2D game-cartridge card: a clip-path cartridge silhouette with layered noisy borders, an inset media well, a label tab, and a vertical Game NFT stamp. From basement.fun.

NEW
Ball Blaster
Ball BlasterAim, charge, fire. Clear the board before it fills.
Game NFT
Mint once, play forever
HOT
Drift Hunters
Drift HuntersTune the car, find the line, hold the slide.
Game NFT
Mint once, play forever
FREE
OvO
OvOWall-jump, dash, slide. A precision platformer.
Game NFT
Mint once, play forever

Usage

example.tsx
import { CompactCartridge } from "~/components/cartridge/compact-cartridge";
<CompactCartridge
title="Ball Blaster"
description="Aim, charge, fire. Clear the board."
media="/cartridge/samples/ball-blaster.png"
topLabel="NEW"
accentColor="#f43f5e"
backgroundColor="#1a0f14"
/>

One shape, stacked four times

The cartridge silhouette is a single clip-path polygon. The trick is layering it: a blurred copy underneath for the drop shadow, a gradient-filled copy for the bevel border, then a slightly-inset copy with the real background on top. A second clip-path carves the media well. Stack them with a 1px offset and the edges read as molded plastic.

A tiled subtle-noise PNG over the fills kills the flat digital look — it's the difference between "div" and "object." The label tab is an inline SVG path tinted with the accent color, and the contrast helper picks black or white text by luminance so the tab stays legible on any accent.

From basement.fun, a project I work on at B3. Game art is placeholder.

The full implementation, across 2 files. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).

components/cartridge/compact-cartridge.tsx
import { useState, type ReactNode } from "react";
import { cn } from "~/lib/utils";
/** Black or white text for contrast against a hex background. */
function contrastText(hex: string): string {
const h = hex.replace("#", "");
const n =
h.length === 3
? h.split("").map((c) => parseInt(c + c, 16))
: [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
const lum = (0.299 * n[0] + 0.587 * n[1] + 0.114 * n[2]) / 255;
return lum > 0.6 ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.92)";
}
export interface CompactCartridgeProps {
title: string;
description: string;
media: string;
topLabel?: string;
topLabelHoverText?: string;
mintText?: string;
buttonText?: string;
accentColor?: string;
backgroundColor?: string;
borderStartColor?: string;
borderEndColor?: string;
shadowColor?: string;
onButtonClick?: () => void;
button?: ReactNode;
className?: string;
}
/**
* A 2D "cartridge" card: a clip-path cartridge silhouette with layered noisy
* borders, an inset media well, a label tab, and a vertical "Game NFT" stamp.
* Trimmed + dependency-free port from basement.fun.
*/
export function CompactCartridge({
title,
description,
media,
topLabel,
topLabelHoverText,
mintText = "Mint once, play forever",
buttonText = "Play game",
accentColor = "#3b82f6",
backgroundColor = "#10121a",
borderStartColor = "rgba(255,255,255,0.3)",
borderEndColor = "rgba(255,255,255,0.1)",
shadowColor = "rgba(0,0,0,0.35)",
onButtonClick,
button,
className,
}: CompactCartridgeProps) {
const [hover, setHover] = useState(false);
const accentText = contrastText(accentColor);
return (
<div
className={cn("relative w-[372px] max-w-full", className)}
style={{ top: topLabel ? 0 : -10 }}
>
{topLabel && (
<div
className="absolute z-30 w-full cursor-help"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<svg viewBox="-16 -4 320 80" className="w-full">
<path
d="m 18 10 v 2 v 15 h 156 v -9 l -13.714 -14.7975 c -1.892 -2.0419 -4.55 -3.2025 -7.335 -3.2025 l -124.951 0 c -5.5228 0 -10 4.4771 -10 10 z"
fill={accentColor}
/>
</svg>
<span
className="absolute left-0 top-[12px] z-10 flex w-1/2 justify-center pl-3 font-mono text-xs uppercase"
style={{ textShadow: "rgba(0,0,0,0.25) 0 -1px 0" }}
>
<span
className="w-[72px] select-none rounded-xl pb-[4px] text-center"
style={{
backgroundColor: accentColor,
color: accentText,
boxShadow: `0 0 10px 0 ${accentColor}`,
}}
>
{topLabel}
</span>
</span>
</div>
)}
<div className="relative" style={{ aspectRatio: "333 / 229" }}>
{/* drop shadow silhouette */}
<div className="absolute left-0 top-2 -z-10 h-full w-full blur-md">
<div
className="clip-path-compact-cartridge h-full w-full"
style={{ background: shadowColor }}
/>
</div>
{/* body: gradient border via clipped stack */}
<div className="clip-path-compact-cartridge absolute left-0 top-0 h-full w-full">
{/* inner fill */}
<div
className="absolute left-px top-px z-10"
style={{ width: "calc(100% - 2px)", height: "calc(100% - 2px)" }}
>
<div
className="clip-path-compact-cartridge subtle-noise h-full w-full"
style={{
backgroundColor,
boxShadow: "inset 12px 16px 14px 10px rgba(0,0,0,0.3)",
}}
/>
</div>
{/* outer border gradient */}
<div
className="h-full w-full"
style={{
background: `linear-gradient(to bottom, ${borderStartColor}, ${borderEndColor})`,
}}
/>
{/* media well */}
<div
className="clip-path-compact-cartridge-media absolute left-0 top-0 z-20 h-[96%] w-[97%]"
style={{
background:
"linear-gradient(to bottom, rgba(255,255,255,0.3), rgba(255,255,255,0.15))",
padding: "1px",
paddingTop: "2px",
}}
>
<div
className="clip-path-compact-cartridge-media subtle-noise absolute left-px top-px"
style={{
width: "calc(100% - 2px)",
height: "calc(100% - 2px)",
backgroundColor,
}}
>
<div
className="absolute left-px top-px"
style={{
zIndex: 1,
width: "calc(100% - 2px)",
height: "calc(100% - 2px)",
boxShadow:
"rgb(0 0 0 / 15%) 18px 11px 20px 22px inset, rgb(0 0 0 / 15%) 18px 50px 36px 22px inset",
}}
/>
{/* content */}
<div className="absolute left-[18%] top-[30%] z-20 flex h-[60%] w-[78%] flex-col items-center justify-center gap-4">
<div className="flex w-full items-center justify-center gap-3">
<div className="size-20 flex-none overflow-hidden rounded-2xl border border-white/15">
<img src={media} alt={title} className="size-full object-cover" />
</div>
<div className="flex flex-grow flex-col gap-1">
<span className="font-semibold text-white drop-shadow-md">{title}</span>
<span
className={cn(
"font-medium text-white/80 drop-shadow-md",
description.length > 70 ? "text-xs" : "text-sm",
)}
>
{description.length > 80 ? `${description.slice(0, 80)}` : description}
</span>
</div>
</div>
{button ?? (
<button
type="button"
onClick={onButtonClick}
className="w-full rounded-xl px-4 py-2 text-sm font-semibold shadow-md transition-[filter] hover:brightness-110 active:translate-y-px"
style={{ backgroundColor: accentColor, color: accentText }}
>
{buttonText}
</button>
)}
</div>
{/* vertical stamp */}
<span
className="absolute bottom-10 left-5 -rotate-90 font-mono font-semibold uppercase drop-shadow-md"
style={{
fontSize: 10,
letterSpacing: 1,
color: "rgba(0,0,0,0.5)",
textShadow: "rgba(255,255,255,0.1) -1px -1px 0",
}}
>
Game NFT
</span>
</div>
{/* mint / hover text */}
<span
className="absolute right-8 top-8 font-mono font-bold uppercase drop-shadow-md transition-opacity"
style={{
fontSize: 10,
letterSpacing: 1,
color: "rgba(0,0,0,0.4)",
textShadow: "rgba(255,255,255,0.1) -1px -1px 0",
}}
>
{hover && topLabelHoverText ? topLabelHoverText : mintText}
</span>
</div>
</div>
</div>
</div>
);
}
app/styles/app.css
/* ── Compact cartridge (2D clip-path card), from basement.fun ───────────── */
.subtle-noise { background-image: url("/cartridge/subtle-noise.png"); background-repeat: repeat; }
.clip-path-compact-cartridge {
clip-path: polygon(
17.006% 10.554%,
17.006% 10.554%,
16.906% 10.164%,
16.777% 9.803%,
16.62% 9.474%,
16.44% 9.179%,
16.237% 8.921%,
16.016% 8.704%,
15.777% 8.53%,
15.525% 8.403%,
15.261% 8.324%,
14.988% 8.297%,
12.745% 8.297%,
12.745% 8.297%,
11.648% 8.433%,
10.608% 8.825%,
9.637% 9.454%,
8.751% 10.297%,
7.963% 11.333%,
7.288% 12.54%,
6.738% 13.898%,
6.328% 15.385%,
6.071% 16.98%,
5.983% 18.661%,
5.983% 89.636%,
5.983% 89.636%,
6.071% 91.317%,
6.328% 92.912%,
6.738% 94.399%,
7.288% 95.757%,
7.963% 96.964%,
8.751% 98%,
9.637% 98.843%,
10.608% 99.472%,
11.648% 99.864%,
12.745% 100%,
93.238% 100%,
93.238% 100%,
94.335% 99.864%,
95.375% 99.472%,
96.346% 98.843%,
97.232% 98%,
98.02% 96.964%,
98.695% 95.757%,
99.245% 94.399%,
99.655% 92.912%,
99.911% 91.317%,
100% 89.636%,
100% 18.661%,
100% 18.661%,
99.911% 16.98%,
99.655% 15.385%,
99.245% 13.898%,
98.695% 12.54%,
98.02% 11.333%,
97.232% 10.297%,
96.346% 9.454%,
95.375% 8.825%,
94.335% 8.433%,
93.238% 8.297%,
38.301% 8.297%,
38.301% 8.297%,
38.028% 8.324%,
37.764% 8.403%,
37.512% 8.53%,
37.273% 8.704%,
37.052% 8.921%,
36.849% 9.179%,
36.669% 9.474%,
36.512% 9.803%,
36.383% 10.164%,
36.283% 10.554%,
36.283% 10.554%,
36.183% 10.944%,
36.054% 11.305%,
35.898% 11.635%,
35.717% 11.929%,
35.515% 12.187%,
35.293% 12.404%,
35.055% 12.578%,
34.803% 12.706%,
34.539% 12.784%,
34.266% 12.811%,
19.023% 12.811%,
19.023% 12.811%,
18.75% 12.784%,
18.486% 12.706%,
18.234% 12.578%,
17.996% 12.404%,
17.774% 12.187%,
17.572% 11.929%,
17.391% 11.635%,
17.235% 11.305%,
17.106% 10.944%,
17.006% 10.554%,
8.72% 19.698%,
8.72% 19.698%,
8.741% 19.29%,
8.803% 18.903%,
8.903% 18.542%,
9.037% 18.212%,
9.201% 17.919%,
9.392% 17.667%,
9.607% 17.463%,
9.843% 17.31%,
10.095% 17.215%,
10.362% 17.182%,
10.362% 17.182%,
10.628% 17.215%,
10.881% 17.31%,
11.116% 17.463%,
11.331% 17.667%,
11.523% 17.919%,
11.687% 18.212%,
11.82% 18.542%,
11.92% 18.903%,
11.982% 19.29%,
12.004% 19.698%,
12.004% 28.65%,
12.004% 28.65%,
11.982% 29.058%,
11.92% 29.445%,
11.82% 29.806%,
11.687% 30.136%,
11.523% 30.429%,
11.331% 30.681%,
11.116% 30.885%,
10.881% 31.038%,
10.628% 31.133%,
10.362% 31.166%,
10.362% 31.166%,
10.095% 31.133%,
9.843% 31.038%,
9.607% 30.885%,
9.392% 30.681%,
9.201% 30.429%,
9.037% 30.136%,
8.903% 29.806%,
8.803% 29.445%,
8.741% 29.058%,
8.72% 28.65%,
8.72% 19.698%
);
}
.clip-path-compact-cartridge-media {
clip-path: polygon(
100% 33.959%,
100% 33.959%,
99.909% 32.186%,
99.646% 30.504%,
99.225% 28.936%,
98.661% 27.503%,
97.967% 26.23%,
97.159% 25.137%,
96.249% 24.248%,
95.254% 23.585%,
94.186% 23.171%,
93.06% 23.028%,
21.852% 23.028%,
21.852% 23.028%,
20.726% 23.171%,
19.659% 23.585%,
18.663% 24.248%,
17.754% 25.137%,
16.945% 26.23%,
16.251% 27.503%,
15.687% 28.936%,
15.266% 30.504%,
15.003% 32.186%,
14.912% 33.959%,
14.912% 40.077%,
14.912% 40.077%,
14.896% 40.814%,
14.849% 41.545%,
14.772% 42.266%,
14.664% 42.976%,
14.526% 43.673%,
14.359% 44.353%,
14.164% 45.015%,
13.94% 45.657%,
13.689% 46.275%,
13.411% 46.868%,
9.934% 53.775%,
9.934% 53.775%,
9.752% 54.164%,
9.587% 54.569%,
9.44% 54.99%,
9.312% 55.424%,
9.203% 55.87%,
9.112% 56.327%,
9.041% 56.793%,
8.99% 57.266%,
8.96% 57.745%,
8.949% 58.229%,
8.949% 92.831%,
8.949% 92.831%,
9.009% 93.994%,
9.181% 95.097%,
9.457% 96.126%,
9.827% 97.065%,
10.282% 97.9%,
10.813% 98.617%,
11.409% 99.2%,
12.062% 99.635%,
12.762% 99.906%,
13.501% 100%,
21.469% 100%,
21.469% 100%,
21.528% 99.999%,
21.587% 99.998%,
21.646% 99.995%,
21.705% 99.991%,
21.763% 99.985%,
21.822% 99.979%,
21.88% 99.971%,
21.938% 99.962%,
21.995% 99.953%,
22.053% 99.942%,
93.06% 99.942%,
93.06% 99.942%,
94.186% 99.798%,
95.254% 99.384%,
96.249% 98.721%,
97.159% 97.832%,
97.967% 96.74%,
98.661% 95.466%,
99.225% 94.034%,
99.646% 92.466%,
99.909% 90.784%,
100% 89.011%,
100% 33.959%
);
}