GitHubX

Stack Menu

PreviousNext

A mobile-friendly bottom menu with stacked items and collapsible sub menus, as an alternative to dropdown menus on small screens.

Bottom-anchored menu built on the dialog primitive. Items animate from a stacked layout into a list; opening a sub menu repositions surrounding rows. Use it where DropdownMenu feels cramped or hover-dependent.

      
      <script setup lang="ts">
import { StackMenu, StackMenuContent, StackMenuGroup, StackMenuItem, StackMenuSub, StackMenuSubContent, StackMenuSubTrigger, StackMenuTrigger } from "@/components/stack-menu";
import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/sonner";
import { EllipsisIcon, HeartIcon, ListMusicIcon, ListPlusIcon, PlusCircleIcon, Share2Icon } from "lucide-vue-next";

function onShare() {
  toast("Shared");
}
</script>

<template>
  <StackMenu>
    <StackMenuTrigger>
      <Button variant="outline" size="icon">
        <EllipsisIcon class="size-4" />
      </Button>
    </StackMenuTrigger>

    <StackMenuContent>
      <StackMenuGroup>
        <StackMenuItem>
          <PlusCircleIcon class="size-4" />
          Add to queue
        </StackMenuItem>

        <StackMenuItem>
          <HeartIcon class="size-4" />
          Save to Liked Songs
        </StackMenuItem>

        <StackMenuSub>
          <StackMenuSubTrigger>
            <ListPlusIcon class="size-4" />
            Add to playlist
          </StackMenuSubTrigger>

          <StackMenuSubContent>
            <StackMenuGroup>
              <StackMenuItem @click="toast('Chill Vibes')">
                <ListMusicIcon class="size-4" />
                Chill Vibes
              </StackMenuItem>
              <StackMenuItem @click="toast('Late Night')">
                <ListMusicIcon class="size-4" />
                Late Night
              </StackMenuItem>
              <StackMenuItem>
                <PlusCircleIcon class="size-4" />
                New playlist
              </StackMenuItem>
            </StackMenuGroup>
          </StackMenuSubContent>
        </StackMenuSub>

        <StackMenuItem @click="onShare">
          <Share2Icon class="size-4" />
          Share
        </StackMenuItem>
      </StackMenuGroup>
    </StackMenuContent>
  </StackMenu>
</template>
    

Features

  • Mobile-first — Opens from the bottom of the viewport with a dimmed overlay
  • Stacked animation — Items transition from stacked to a full list layout
  • Collapsible sub menusStackMenuSub hides sibling items and expands nested StackMenuGroup content
  • Composable — Same mental model as other menu primitives (trigger, content, group, item)

StackMenu vs ActionSheet

Both components slide up from the bottom with an overlay, but they solve different jobs.

ActionSheet is modeled after the iOS action sheet: a short, focused list of mutually meaningful choices—often a primary decision, destructive option, or cancel. It behaves like a blocking confirmation surface (similar in spirit to an alert dialog): you typically pick one outcome or dismiss, and it works well with a promise API (start()) when async code needs to know what the user chose.

StackMenu is a navigation-style overflow menu for a screen or row: many commands, optional sub menus, and the same item can stay available while you drill in and out. It replaces cramped dropdown patterns on touch devices rather than posing a single “what do you want to do?” prompt.

Use ActionSheet when the flow is “choose one action or cancel.” Use StackMenu when the flow is “open a menu of commands (and possibly nested lists) from a trigger.”

See also: Action Sheet.

Installation

Install from the Vuzeno registry with the shadcn-vue CLI:

      
      bunx --bun shadcn-vue@latest add https://vuzeno.com/r/stack-menu.json
    

Anatomy

      
      <template>
  <StackMenu>
    <StackMenuTrigger />

    <StackMenuContent>
      <StackMenuGroup>
        <StackMenuItem />

        <StackMenuSub>
          <StackMenuSubTrigger />

          <StackMenuSubContent>
            <StackMenuGroup>
              <StackMenuItem />
            </StackMenuGroup>
          </StackMenuSubContent>
        </StackMenuSub>
      </StackMenuGroup>
    </StackMenuContent>
  </StackMenu>
</template>
    

Examples

Basic usage

Control visibility with StackMenuTrigger and compose rows with StackMenuItem inside StackMenuGroup.

      
      <script setup lang="ts">
import {
  StackMenu,
  StackMenuContent,
  StackMenuGroup,
  StackMenuItem,
  StackMenuTrigger,
} from "@vuzeno/registry/ui/stack-menu";
import { Button } from "@/components/button";
</script>

<template>
  <StackMenu>
    <StackMenuTrigger>
      <Button variant="outline">Open</Button>
    </StackMenuTrigger>

    <StackMenuContent>
      <StackMenuGroup>
        <StackMenuItem>Edit</StackMenuItem>
        <StackMenuItem>Duplicate</StackMenuItem>
        <StackMenuItem>Delete</StackMenuItem>
      </StackMenuGroup>
    </StackMenuContent>
  </StackMenu>
</template>
    

With sub menu

Wrap expandable sections in StackMenuSub. The trigger must be StackMenuSubTrigger; nested items live in StackMenuSubContent.

      
      <script setup lang="ts">
import { StackMenu, StackMenuContent, StackMenuGroup, StackMenuItem, StackMenuSub, StackMenuSubContent, StackMenuSubTrigger, StackMenuTrigger } from "@/components/stack-menu";
import { Button } from "@/components/ui/button";
import { ChevronRightIcon, FolderIcon, HomeIcon, SettingsIcon, UserIcon } from "lucide-vue-next";
</script>

<template>
  <StackMenu>
    <StackMenuTrigger>
      <Button variant="outline">
        Open menu
      </Button>
    </StackMenuTrigger>

    <StackMenuContent>
      <StackMenuGroup>
        <StackMenuItem>
          <HomeIcon class="size-4" />
          Home
        </StackMenuItem>

        <StackMenuSub>
          <StackMenuSubTrigger>
            <FolderIcon class="size-4" />
            Projects
          </StackMenuSubTrigger>

          <StackMenuSubContent>
            <StackMenuGroup>
              <StackMenuItem>
                <ChevronRightIcon class="size-4" />
                Web app
              </StackMenuItem>
              <StackMenuItem>
                <ChevronRightIcon class="size-4" />
                Mobile
              </StackMenuItem>
              <StackMenuItem>
                <ChevronRightIcon class="size-4" />
                Design system
              </StackMenuItem>
            </StackMenuGroup>
          </StackMenuSubContent>
        </StackMenuSub>

        <StackMenuItem>
          <UserIcon class="size-4" />
          Account
        </StackMenuItem>

        <StackMenuItem>
          <SettingsIcon class="size-4" />
          Settings
        </StackMenuItem>
      </StackMenuGroup>
    </StackMenuContent>
  </StackMenu>
</template>
    
      
      <template>
  <StackMenuContent>
    <StackMenuGroup>
      <StackMenuItem>Home</StackMenuItem>

      <StackMenuSub>
        <StackMenuSubTrigger>Projects</StackMenuSubTrigger>

        <StackMenuSubContent>
          <StackMenuGroup>
            <StackMenuItem>Web app</StackMenuItem>
            <StackMenuItem>Mobile</StackMenuItem>
          </StackMenuGroup>
        </StackMenuSubContent>
      </StackMenuSub>

      <StackMenuItem>Settings</StackMenuItem>
    </StackMenuGroup>
  </StackMenuContent>
</template>
    

Limitations

  • No group labelsStackMenuGroup does not provide a label slot; use plain items or custom markup if needed later.
  • No scrolling — Long lists are not scrollable; excess items can be clipped at the bottom of the screen.
  • Single open sub menu — Only one StackMenuSub should be expanded at a time; multiple open subs are not supported.
  • Fixed row height — Layout math assumes a constant item height; deeply custom content sizes may misalign animations.