Skip to content
← components

App Icon

new

A glossy iOS-style squircle app icon: SVG squircle clip, a top sheen, an inset rim, and a glare that sweeps on hover. Extracted from explorer.b3.fun.

B3
HypeDuel
Quests
PewPew
Gamesbond
NFT Create

hover an icon for the glare

Usage

example.tsx
import { AppIcon } from "~/components/app-icon/app-icon";
<AppIcon src="/icon.png" alt="My App" label="My App" size={88} />

A squircle, a sheen, and a glare

The shape is a proper squircle, not a rounded rectangle. A rounded rect has a visible seam where the straight edge meets the corner arc; a squircle curves continuously, which is why app icons use it. It's an SVG clipPath with clipPathUnits="objectBoundingBox", so the single path scales to any icon size.

On top of the image sit three cheap layers: a soft white gradient over the upper half for a sheen, an inset box-shadow that's bright on the top-left and dark on the bottom-right so the icon reads as slightly domed, and a skewed white gradient parked off to the left that slides across on hover for a glare. The parent's clip path traps all of it inside the squircle.

app-icon.tsx
{/* objectBoundingBox clip path scales the squircle to any size */}
<clipPath id="app-squircle" clipPathUnits="objectBoundingBox">
<path d="M.5,0 C.9,0 1,.09 1,.5 C1,.91 .9,1 .5,1 C.09,1 0,.9 0,.5 C0,.09 .09,0 .5,0Z" />
</clipPath>
{/* the icon: clip + top sheen + inset rim + a glare that sweeps on hover */}
<div style={{ clipPath: "url(#app-squircle)", backgroundImage }} className="group …">
<div className="… bg-gradient-to-b from-white/15 to-transparent" />
<div style={{ boxShadow:
"inset 4px 10px 10px -4px rgba(255,255,255,.3), inset -4px -10px 10px -4px rgba(0,0,0,.3)" }} />
<div className="… -translate-x-full -skew-x-[15deg] group-hover:translate-x-full
bg-gradient-to-r from-transparent via-white/35 to-transparent" />
</div>

Props

proptypedefaultdescription
srcstringIcon image URL.
altstringAccessible label.
labelReactNodeCaption under the icon.
mediaReactNodeOverlay inside the squircle (e.g. a video).
sizenumber88Icon size in px.

From explorer.b3.fun, a project I work on at B3.

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

components/app-icon/app-icon.tsx
import type { ReactNode } from "react";
import { cn } from "~/lib/utils";
// Smooth iOS-style squircle as an objectBoundingBox clip path (scales to any size).
const SQUIRCLE =
"M0.5,0 C0.90,0 1,0.09 1,0.5 C1,0.91 0.9,1 0.5,1 C0.09,1 0,0.9 0,0.5 C0,0.09 0.09,0 0.5,0Z";
export interface AppIconProps {
src: string;
alt: string;
label?: ReactNode;
/** Overlay inside the squircle (e.g. a video or badge). */
media?: ReactNode;
/** Icon size in px. */
size?: number;
className?: string;
}
/**
* A glossy "squircle" app icon: SVG squircle clip, a top sheen, an inset rim
* highlight/shadow, and a glare that sweeps across on hover. Extracted from
* explorer.b3.fun.
*/
export function AppIcon({
src,
alt,
label,
media,
size = 88,
className,
}: AppIconProps) {
return (
<figure className={cn("flex flex-col items-center gap-2", className)}>
<svg width="0" height="0" className="absolute" aria-hidden>
<defs>
<clipPath id="app-squircle" clipPathUnits="objectBoundingBox">
<path d={SQUIRCLE} />
</clipPath>
</defs>
</svg>
<div
className="group relative overflow-hidden bg-cover bg-center transition-transform duration-300 hover:scale-105"
style={{
width: size,
height: size,
clipPath: "url(#app-squircle)",
backgroundImage: `url(${src})`,
}}
role="img"
aria-label={alt}
>
{/* top sheen */}
<div className="absolute inset-x-0 top-0 h-1/2 bg-gradient-to-b from-white/15 to-transparent" />
{media && <div className="absolute inset-0">{media}</div>}
{/* inset rim: bright top-left, dark bottom-right */}
<div
className="absolute inset-0"
style={{
boxShadow:
"inset 4px 10px 10px -4px rgba(255,255,255,0.3), inset -4px -10px 10px -4px rgba(0,0,0,0.3)",
}}
/>
{/* glare sweep on hover (clipped by the parent squircle) */}
<div className="absolute inset-0 opacity-0 transition-opacity duration-500 group-hover:opacity-100">
<div className="absolute inset-0 -translate-x-full -skew-x-[15deg] bg-gradient-to-r from-transparent via-white/35 to-transparent transition-transform duration-700 ease-out group-hover:translate-x-full" />
</div>
</div>
{label && (
<figcaption className="max-w-[10ch] truncate text-center text-xs text-[hsl(var(--muted-foreground))]">
{label}
</figcaption>
)}
</figure>
);
}