Cards

App Download Stack

An expandable macOS-style app selector that fanned-stack animates icons and drives a multi-app download flow.

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

TitleStarter Mac
Selected Apps
Expanded

Props

Prop Type Default Description
appsAppDownloadStackApp[]undefinedList 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).
titlestring'Starter Mac'Selector header text shown when expanded.
selectedAppsnumber[]undefinedControlled selection of app ids. Leave undefined for uncontrolled.
expandedbooleanundefinedControlled expansion. Leave undefined for uncontrolled.
classstringundefinedMerged onto the outer column wrapper.

Events

Event Payload Description
change(selected: number[]) => voidEmitted when selection changes.
download(selected: number[]) => voidEmitted when Download Selected is clicked with the current selected ids.
expand-change(expanded: boolean) => voidEmitted when expansion toggles.
update:expanded(expanded: boolean) => voidv-model update for expansion.
update:selectedApps(selected: number[]) => voidv-model update for selection.

Slots

Slot Props Description
default-No default slot is used.

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
uiobject-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 variants callbacks (reading custom index) and computed per state; when reduced motion, all variants collapse to rest/flat forms.
  • The shine sweep uses useAnimationControls().start() called after nextTick() so the target node exists; sweep runs infinitely during download with linear easing and is stopped on completion.
  • Download sequence uses two setTimeouts: one for DOWNLOAD_DURATION_MS (3000) to complete, one RESET_DELAY_MS (1000) to collapse/reset. Timers are cleared on unmount.
  • layoutId is only applied when motion is not reduced (prevents layout animation noise under reduced motion).
  • Panel transitions use AnimatePresence with 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.
Pass custom 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.
Copyright © 2026