Base components
App store badge
A download link for a mobile or desktop app store that shows the store's official badge artwork unchanged, so its only motion is the focus ring and a small press.
Live preview
Loading preview
A download section with App Store and Google Play badges side by side in brand style, the same pair in outline style on a dark band, and a row of all five stores at size md. The demo draws neutral placeholder badges in the slot. Replay presses the first badge.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add app-store-badgeUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { AppStoreBadge } from './app-store-badge';
import type { AppStore } from './app-store-badge-variants';
const STORES: { store: AppStore; name: string }[] = [
{ store: 'app-store', name: 'App Store' },
{ store: 'google-play', name: 'Google Play' },
{ store: 'mac-app-store', name: 'Mac App Store' },
{ store: 'galaxy-store', name: 'Galaxy Store' },
{ store: 'app-gallery', name: 'AppGallery' },
];
// A neutral placeholder: an app passes the store's official badge artwork instead.
function PlaceholderArtwork({ name }: { name: string }) {
return (
<span className="flex items-center justify-center gap-2 px-3 text-xs font-semibold whitespace-nowrap text-white in-data-[style=outline]:text-fg-primary">
<svg
aria-hidden="true"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.67"
className="size-5 shrink-0"
>
<rect x="3" y="3" width="14" height="14" rx="4" />
<path d="M10 7v6M7 10h6" strokeLinecap="round" />
</svg>
{name}
</span>
);
}
export default function AppStoreBadgeDemo({ replay = 0 }: { replay?: number }) {
const [pressed, setPressed] = React.useState(false);
React.useEffect(() => {
if (replay === 0) return;
setPressed(true);
const timer = window.setTimeout(() => setPressed(false), 240);
return () => window.clearTimeout(timer);
}, [replay]);
return (
<div className="flex w-full max-w-3xl flex-col gap-8">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<p className="text-lg font-semibold text-primary">Get the mobile app</p>
<p className="text-sm text-tertiary">
Track your tasks on your phone and get alerts when something changes.
</p>
</div>
<div className="flex flex-wrap gap-2">
{STORES.slice(0, 2).map(({ store, name }, index) => (
<AppStoreBadge
key={store}
store={store}
size="lg"
href="#"
data-pressed={index === 0 && pressed ? '' : undefined}
>
<PlaceholderArtwork name={name} />
</AppStoreBadge>
))}
</div>
</div>
<div className="dark flex flex-wrap gap-2 rounded-xl bg-primary p-6">
{STORES.slice(0, 2).map(({ store, name }) => (
<AppStoreBadge key={store} store={store} size="lg" badgeStyle="outline" href="#">
<PlaceholderArtwork name={name} />
</AppStoreBadge>
))}
</div>
<div className="flex flex-wrap gap-2">
{STORES.map(({ store, name }) => (
<AppStoreBadge key={store} store={store} href="#">
<PlaceholderArtwork name={name} />
</AppStoreBadge>
))}
</div>
</div>
);
}
API
Parts: root (link), badge artwork (img or inline SVG supplied by the app), outline (Outline style).
| Prop | Type | Default | Description |
|---|---|---|---|
| store | "app-store" | "mac-app-store" | "google-play" | "galaxy-store" | "app-gallery" | undefined | Required. The store; sets the default label and the badge width. |
| href | string | undefined | Required. The app's page in the store. |
| size | "md" | "lg" | "md" | 40 or 44px tall; the width follows the store artwork (for example 120 or 132px for the App Store). |
| badgeStyle | "brand" | "outline" | "brand" | brand: the black badge with color artwork. outline: a transparent badge with one-color artwork and a 1px fg-primary outline, for light and dark pages. Called badgeStyle because style is a reserved attribute. |
| label | string | the store's badge text | Accessible name. Defaults to the text on the artwork, for example "Download on the App Store" or "Get it on Google Play". |
| Slot | Description |
|---|---|
| default | Required. The official badge artwork for the store and style, which the app downloads from the store's marketing pages. Lunia ships no store artwork. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| ASB1 | press (pointer down) | root | scale | 1 → scale-press | duration-sm (200)ease-out | Stays still |
| ASB2 | release | root | scale | scale-press → 1 | duration-sm (200)ease-out | Stays still |
| ASB3 | keyboard focus | root | box-shadow | none → focus ring (2px bg-primary gap, 4px focus-ring) | duration-2xs (120)ease-out | Shows at once |
Accessibility
- Native link with the accessible name set to label; the artwork is aria-hidden. The default label matches the visible badge text (WCAG 2.5.3 label in name).
- No hover and no disabled state: the artwork must not be recolored or faded. When the app is not in a store, do not render that badge.
- Links that open the store in a new tab add "(opens in a new tab)" to the name.
- Both sizes are 40px or taller, so the target meets the 24 x 24px minimum (WCAG 2.5.8).
- Reduced motion: the badge does not scale.