Menu

Change language

GitHub repo

Profile Menu

blocklayout

Header-grade profile/account dropdown. Child-snippet DropdownMenu the consumer wraps around their own avatar button (default trigger ships an Avatar fallback). Sections: upgrade, account/billing/notifications/settings, log out. Calls `onselect` with the chosen item key so consumers wire navigation and auth themselves.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/profile-menu.json
Manifest:https://uipkge.dev/r/svelte/profile-menu.jsonInstalls to:src/lib/components/blocks/profile-menu/

Variants

Loading interactive previews…

npm dependencies

Files installed (1)

  • src/lib/components/blocks/profile-menu/ProfileMenu.svelte4.3 kB
    <script lang="ts" module>
      import type { Snippet } from 'svelte'
    
      export interface ProfileMenuUser {
        name: string
        email: string
        avatar?: string
      }
    
      export type ProfileMenuKey = 'upgrade' | 'account' | 'billing' | 'notifications' | 'settings' | 'logout'
    
      export interface ProfileMenuProps {
        user?: ProfileMenuUser
        /** Custom trigger. Receives the user, the computed initials, and the
         *  trigger props to spread onto your own element (Svelte has no automatic
         *  as-child merge, so spreading `props` is what wires the toggle). */
        children?: Snippet<[{ user: ProfileMenuUser; initials: string; props: Record<string, unknown> }]>
        onselect?: (key: ProfileMenuKey) => void
      }
    </script>
    
    <script lang="ts">
      import { BadgeCheck, Bell, CreditCard, LogOut, Settings, Zap } from '@lucide/svelte'
      import { Avatar, AvatarFallback, AvatarImage } from '$lib/components/ui/avatar'
      import {
        DropdownMenu,
        DropdownMenuContent,
        DropdownMenuGroup,
        DropdownMenuItem,
        DropdownMenuLabel,
        DropdownMenuSeparator,
        DropdownMenuTrigger,
      } from '$lib/components/ui/dropdown-menu'
    
      let {
        user = { name: 'Alex Morgan', email: 'alex@example.com', avatar: '' },
        children,
        onselect,
      }: ProfileMenuProps = $props()
    
      const initials = $derived.by(() => {
        const parts = user.name.trim().split(/\s+/).slice(0, 2)
        return parts.map((p) => p[0]?.toUpperCase()).join('') || 'U'
      })
    </script>
    
    <!-- DropdownMenu renders no DOM of its own, so the slot marker lives on a
         display:contents wrapper that vanishes from layout. -->
    <div data-slot="profile-menu" class="contents">
      <DropdownMenu>
        {#if children}
          <DropdownMenuTrigger>
            {#snippet child({ props })}
              {@render children({ user, initials, props })}
            {/snippet}
          </DropdownMenuTrigger>
        {:else}
          <DropdownMenuTrigger
            class="hover:bg-accent focus-visible:ring-ring flex size-8 items-center justify-center rounded-lg transition-colors focus-visible:ring-1 focus-visible:outline-none"
            aria-label="Open profile menu"
          >
            <Avatar class="size-7">
              {#if user.avatar}
                <AvatarImage src={user.avatar} alt={user.name} />
              {/if}
              <AvatarFallback class="bg-primary/10 text-primary text-xs font-semibold">
                {initials}
              </AvatarFallback>
            </Avatar>
          </DropdownMenuTrigger>
        {/if}
        <DropdownMenuContent class="w-60 rounded-lg" align="end" sideOffset={8}>
          <DropdownMenuLabel class="p-0 font-normal">
            <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
              <Avatar class="size-9 rounded-lg">
                {#if user.avatar}
                  <AvatarImage src={user.avatar} alt={user.name} />
                {/if}
                <AvatarFallback class="bg-primary/10 text-primary rounded-lg text-xs font-semibold">
                  {initials}
                </AvatarFallback>
              </Avatar>
              <div class="grid flex-1 text-left leading-tight">
                <span class="truncate text-sm font-semibold">{user.name}</span>
                <span class="text-muted-foreground truncate text-xs">{user.email}</span>
              </div>
            </div>
          </DropdownMenuLabel>
          <DropdownMenuSeparator />
          <DropdownMenuGroup>
            <DropdownMenuItem onSelect={() => onselect?.('upgrade')}>
              <Zap class="size-4" />
              Upgrade to Pro
            </DropdownMenuItem>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuGroup>
            <DropdownMenuItem onSelect={() => onselect?.('account')}>
              <BadgeCheck class="size-4" />
              Account
            </DropdownMenuItem>
            <DropdownMenuItem onSelect={() => onselect?.('billing')}>
              <CreditCard class="size-4" />
              Billing
            </DropdownMenuItem>
            <DropdownMenuItem onSelect={() => onselect?.('notifications')}>
              <Bell class="size-4" />
              Notifications
            </DropdownMenuItem>
            <DropdownMenuItem onSelect={() => onselect?.('settings')}>
              <Settings class="size-4" />
              Settings
            </DropdownMenuItem>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem onSelect={() => onselect?.('logout')}>
            <LogOut class="size-4" />
            Log out
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
    

Raw manifest:https://uipkge.dev/r/svelte/profile-menu.json