Anatomy of the Store Badge
An inline-SVG lockup that scales off one height prop, a sheen that crosses once instead of looping, and a QR popover that only exists for fine pointers.
Download badges are usually a <img src="app-store-badge.svg"> — a fixed
bitmap you can't restyle or resize cleanly. StoreBadge draws the lockup
itself: one height prop drives the icon, the two lines of text, and the
padding, so the badge scales like a font instead of an image.
The lockup, not a logo image
- Icon slot
- iconSize = height × 0.52
- Two-line label
- caption (0.17h) + name (0.3h), stacked
const iconSize = Math.round(height * 0.52);
const topSize = Math.max(9, Math.round(height * 0.17));
const nameSize = Math.max(13, Math.round(height * 0.3));
<a
className="group/badge relative isolate inline-flex select-none items-center overflow-hidden ..."
style={{
height: `${height}px`,
gap: `${Math.round(height * 0.17)}px`,
paddingInline: `${Math.round(height * 0.3)}px`,
borderRadius: `${Math.round(height * 0.18)}px`,
}}
>
{store === "app-store" ? <AppleLogo size={iconSize} /> : <GooglePlayLogo size={iconSize} />}
<span className="flex flex-col items-start leading-none">
<span style={{ fontSize: `${topSize}px` }}>{copy.top}</span>
<span style={{ fontSize: `${nameSize}px` }}>{copy.name}</span>
</span>
</a>Every dimension — icon, gap, padding, corner radius, both text sizes — is a
ratio of height, floored with Math.max so the caption never drops below a
legible 9px. Hand it height={72} for a hero and height={40} for a footer;
nothing else changes, because nothing else is a hardcoded pixel value.
The sheen, once per hover
- Lift
- translateY −2px, 200ms cubic-bezier(0.22,1,0.36,1)
- Sheen
- skewed strip, −130% → 130%, 700ms ease-out
<span
aria-hidden
className="pointer-events-none absolute inset-0 -translate-x-[130%] skew-x-[-12deg] transition-transform duration-700 ease-out group-hover/badge:translate-x-[130%] motion-reduce:hidden"
style={{ background: "linear-gradient(105deg, transparent 20%, rgba(255,255,255,0.28) 50%, transparent 80%)" }}
/>This isn't a shimmer that loops on an interval — it's a single translate-x
transition parked off-screen left (-130%) that eases to off-screen right
(130%) the moment group-hover is true, and snaps back to -130% the
instant the pointer leaves (no exit transition is defined, so it resets
instantly, ready for the next hover). The -12deg skew is what makes it read
as a light streak crossing a curved badge edge instead of a rectangular bar
sliding across a rectangular one.
The QR is a popover, not a modal
const mq = window.matchMedia("(pointer: coarse)");
const show = () => !coarse && setOpen(true);
<span onMouseEnter={show} onMouseLeave={hide} onFocusCapture={show} onBlurCapture={hide}>
{children}
<AnimatePresence>
{open ? (
<motion.span
initial={{ opacity: 0, y: 8, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.96 }}
transition={{ type: "spring", stiffness: 520, damping: 32 }}
role="dialog"
aria-label={label}
>
<QrCode value={url} size={size} logo={logo} label={label} />
</motion.span>
) : null}
</AnimatePresence>
</span>matchMedia("(pointer: coarse)") gates the whole feature: on touch, show
is a no-op and the badge just links normally — there's no tap-to-reveal state
to get stuck in on a phone. On a fine pointer, hover or focus (so keyboard
users get it too) mounts the AnimatePresence child, which pops in on the
same { stiffness: 520, damping: 32 } spring used everywhere else in the
library for menus and popovers. The QR itself is generated client-side from
the badge's href (or an explicit qrUrl) — no image asset, no server round
trip.
Accessibility
The badge is a real <a> the whole way through — target/rel pass through
normally, and the sheen has motion-reduce:hidden so it never renders under
reduced motion rather than just skipping its transition. The QR popover is
role="dialog" with an aria-label, and because it opens on onFocusCapture
as well as onMouseEnter, tabbing to the badge reveals the same panel a mouse
hover would.
The result
One height prop drives an entire scalable lockup, a sheen that crosses once
per hover instead of looping, and a QR popover that only exists where a phone
camera could actually use it — hover either badge below to see all three.