App Download Stack
App Download Stack starts as a collapsed fan of app icons. Clicking expands a selectable grid; you can toggle apps (up to maxPinned-like cap implied by selection), then trigger a download sequence that shows a downloading state with a shine sweep and finally a completion state before auto-resetting. It respects prefers-reduced-motion, supports controlled expanded/selectedApps, and emits lifecycle events.
Usage
Provide apps with id, name, icon (a Nuxt UI icon name). The trigger is a plain button that expands the selector. Use v-model:expanded and v-model:selectedApps to control state, or listen to expand-change, change, download events.
<template>
<div class="flex h-80 w-full items-center justify-center">
<MAppDownloadStack
title="My Apps"
:apps="apps"
/>
</div>
</template>
<script setup lang="ts">
const apps = [
{ id: 1, name: 'Safari', icon: 'i-lucide-compass' },
{ id: 2, name: 'Photos', icon: 'i-lucide-image' },
{ id: 3, name: 'Podcasts', icon: 'i-lucide-mic' },
{ id: 4, name: 'Calendar', icon: 'i-lucide-calendar-days' },
]
</script>
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
apps | AppDownloadStackApp[] | undefined | List of apps to render. Defaults to built-in placeholder macOS list if undefined. Each AppDownloadStackApp has id (number), name (string), icon (string, a Nuxt UI icon name such as i-lucide-compass). |
title | string | 'Starter Mac' | Selector header text shown when expanded. |
selectedApps | number[] | undefined | Controlled selection of app ids. Leave undefined for uncontrolled. |
expanded | boolean | undefined | Controlled expansion. Leave undefined for uncontrolled. |
class | string | undefined | Merged onto the outer column wrapper. |
Events
| Event | Payload | Description |
|---|---|---|
change | (selected: number[]) => void | Emitted when selection changes. |
download | (selected: number[]) => void | Emitted when Download Selected is clicked with the current selected ids. |
expand-change | (expanded: boolean) => void | Emitted when expansion toggles. |
update:expanded | (expanded: boolean) => void | v-model update for expansion. |
update:selectedApps | (selected: number[]) => void | v-model update for selection. |
Slots
MAppDownloadStack exposes no slots. The stack, its cards and the labels are all
rendered from props.
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.appDownloadStack in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | flex h-auto flex-col items-center justify-center |
wrapper | - | flex flex-col items-center justify-center |
trigger | - | group relative isolate flex h-16 w-16 cursor-pointer items-center justify-center |
stackIcon | - | absolute inset-0 rounded-xl border-none |
selector | - | flex flex-col items-center gap-2 |
selectorHeader | - | flex w-full cursor-pointer items-center justify-between px-0.5 |
title | - | my-0 font-medium leading-none text-highlighted |
countWrap | - | flex items-center gap-1 |
count | - | my-0 font-medium leading-none text-highlighted |
chevron | - | size-4 text-muted |
grid | - | grid grid-cols-2 gap-3 |
tile | - | relative flex h-[80px] w-[80px] |
check | - | pointer-events-none absolute top-2 right-2 z-[1] flex h-4 w-4 items-center justify-center rounded-full border border-solid |
checkmark | - | z-[1] h-3 w-3 |
app | - | group flex h-full w-full cursor-pointer flex-col items-center justify-center gap-1 rounded-xl border-2 border-transparent bg-default/80 p-2 transition-all duration-200 hover:border-primary |
appIcon | - | rounded-lg |
appName | - | font-medium text-highlighted text-xs |
download | - | mt-4 w-full cursor-pointer rounded-lg bg-primary px-4 py-2 font-semibold text-inverted shadow transition hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50 |
downloading | - | flex flex-col items-center gap-4 |
downloadingWrap | - | relative flex h-16 w-16 items-center justify-center |
shine | - | absolute inset-0 rounded-xl bg-primary/20 |
downloadingIcon | - | absolute inset-0 rounded-xl border-none |
downloadingLabel | - | font-semibold text-primary |
complete | - | flex flex-col items-center gap-4 |
completeLabel | - | font-semibold text-success |
Accessibility
The collapsed trigger is a plain <button> with aria-label="Expand app selection". When expanded, the header is also a button (collapses on click) and each app tile is a button with :aria-pressed="isSelected(app.id)". Checkmark uses a decorative SVG with <title>Checkmark</title>. The download button is disabled when nothing selected. The stack and downloading icons carry role="img" with the app name as the accessible label; the grid tiles are labelled by their visible text. No ARIA live regions announce download state changes; focus is not managed between states. prefers-reduced-motion replaces all motion with near-zero duration variants (floats/rotations become flat). Layout transitions are disabled when reduced motion is preferred (:layout="!shouldReduceMotion").
Performance
- Icon motion is expressed via
variantscallbacks (readingcustomindex) and computed per state; when reduced motion, all variants collapse torest/flat forms. - The shine sweep uses
useAnimationControls().start()called afternextTick()so the target node exists; sweep runs infinitely during download with linear easing and is stopped on completion. - Download sequence uses two
setTimeouts: one forDOWNLOAD_DURATION_MS(3000) to complete, oneRESET_DELAY_MS(1000) to collapse/reset. Timers are cleared on unmount. layoutIdis only applied when motion is not reduced (prevents layout animation noise under reduced motion).- Panel transitions use
AnimatePresencewith scale/opacity; reduced motion uses opacity-only or zero-duration variants. - No WebGL, heavy loops, or per-frame state churn beyond timers and the small computed maps.
apps with Nuxt UI icon names (any i-… Iconify id). The default placeholder icons are Nuxt UI icons, so no external asset request is made.