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.
↓Scroll to step through it
The lockup, not a logo image
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.
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
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. 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.
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>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.
- 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));<aclassName="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>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.
- 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));<aclassName="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>The lockup, not a logo image
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.
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.
- Lift
- translateY −2px, 200ms cubic-bezier(0.22,1,0.36,1)
- Sheen
- skewed strip, −130% → 130%, 700ms ease-out
<spanaria-hiddenclassName="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%)" }}/>The sheen, once per hover
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. 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.
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>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.
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.
Motion Score
translatePosition / lift via translatescaleScale spring / pressrotateRotationopacityFade / cross-fade