App Icon
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.
hover an icon for the glare
Usage
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.
{/* 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-fullbg-gradient-to-r from-transparent via-white/35 to-transparent" /></div>
Props
| prop | type | default | description |
|---|---|---|---|
| src | string | — | Icon image URL. |
| alt | string | — | Accessible label. |
| label | ReactNode | — | Caption under the icon. |
| media | ReactNode | — | Overlay inside the squircle (e.g. a video). |
| size | number | 88 | Icon size in px. |
From explorer.b3.fun, a project I work on at B3.
Source
download .zipThe full implementation. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).
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><divclassName="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 */}<divclassName="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>);}