Menu

Change language

GitHub repo

Dashboard Layout

blocklayout

Full dashboard shell — collapsible sidebar (sidebar-02) + sticky topbar (sidebar trigger, breadcrumb, command palette, theme switch, notifications, profile menu) + main content slot. Drop it in a SvelteKit layout: `<DashboardLayout>{@render children?.()}</DashboardLayout>`. Auto-pulls every transitive piece (sidebar-02, command-palette, notifications-popover, profile-menu, theme-switch).

Installation

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

Variants

Loading interactive previews…

Files installed (1)

  • src/lib/components/blocks/dashboard-layout/DashboardLayout.svelte5.6 kB
    <script lang="ts" module>
      import type { Snippet } from 'svelte'
    
      export interface DashboardLayoutCrumb {
        label: string
        href?: string
      }
    
      export interface DashboardLayoutProps {
        breadcrumbs?: DashboardLayoutCrumb[]
        user?: { name: string; email: string; avatar?: string }
        onProfileSelect?: (key: string) => void
        onCommandSelect?: (item: { label: string; hint?: string }) => void
        children?: Snippet
      }
    </script>
    
    <script lang="ts">
      import { Bell } from '@lucide/svelte'
      import Sidebar02 from '$lib/components/blocks/sidebar-02/Sidebar02.svelte'
      import CommandPalette from '$lib/components/blocks/command-palette/CommandPalette.svelte'
      import NotificationsPopover from '$lib/components/blocks/notifications-popover/NotificationsPopover.svelte'
      import { Button } from '$lib/components/ui/button'
      import { Separator } from '$lib/components/ui/separator'
      import {
        Breadcrumb,
        BreadcrumbItem,
        BreadcrumbLink,
        BreadcrumbList,
        BreadcrumbPage,
        BreadcrumbSeparator,
      } from '$lib/components/ui/breadcrumb'
      import { SidebarInset, SidebarProvider, SidebarTrigger } from '$lib/components/ui/sidebar'
      import { ThemeSwitch, type ThemeSwitchTheme } from '$lib/components/ui/theme-switch'
    
      let {
        breadcrumbs = [{ label: 'Dashboard' }],
        user = { name: 'Alex Morgan', email: '[email protected]' },
        onProfileSelect,
        onCommandSelect,
        children,
      }: DashboardLayoutProps = $props()
    
      // The Svelte registry ships no use-theme hook (yet), so the shell owns a
      // tiny local theme state and applies the dark class itself. Swap this for
      // your own theme store after install.
      type Theme = 'light' | 'dark' | 'system'
      let theme = $state<Theme>('system')
    
      $effect(() => {
        const root = document.documentElement
        const dark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
        root.classList.toggle('dark', dark)
      })
    
      // ThemeSwitch can emit 'black' but this shell only models light/dark/system;
      // fall back to dark so the control still works in this block.
      function onThemeChange(t: ThemeSwitchTheme) {
        theme = t === 'black' ? 'dark' : t
      }
    
      // `user` + `onProfileSelect` are accepted for API parity with the Vue twin
      // (consumers wire them into NavUser or a custom header slot) but intentionally
      // unused here: the header shows no avatar because the sidebar-02 footer's
      // NavUser already owns the profile affordance.
    </script>
    
    <SidebarProvider data-slot="dashboard-layout">
      <a
        href="#main-content"
        class="bg-background text-foreground ring-ring sr-only z-50 rounded-md text-sm font-medium shadow-md ring-2 focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:px-3 focus:py-2"
        >Skip to content</a
      >
      <Sidebar02 />
      <SidebarInset>
        <header
          class="bg-background/80 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-30 flex h-14 w-full shrink-0 items-center justify-between border-b px-4 backdrop-blur-xl transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12"
        >
          <div class="flex items-center gap-2">
            <SidebarTrigger class="-ml-1" />
            <Separator orientation="vertical" class="mr-2 h-4" />
            <Breadcrumb>
              <BreadcrumbList>
                {#each breadcrumbs as crumb, i (i)}
                  <BreadcrumbItem class={i === 0 ? 'hidden md:block' : ''}>
                    {#if crumb.href && i < breadcrumbs.length - 1}
                      <BreadcrumbLink
                        href={crumb.href}
                        class="text-muted-foreground/70 hover:text-foreground transition-colors"
                      >
                        {crumb.label}
                      </BreadcrumbLink>
                    {:else}
                      <BreadcrumbPage class="font-medium">{crumb.label}</BreadcrumbPage>
                    {/if}
                  </BreadcrumbItem>
                  {#if i < breadcrumbs.length - 1}
                    <BreadcrumbSeparator class={i === 0 ? 'hidden md:block' : ''} />
                  {/if}
                {/each}
              </BreadcrumbList>
            </Breadcrumb>
          </div>
          <div class="flex items-center gap-1 px-2 sm:gap-3">
            <CommandPalette onSelect={(item) => onCommandSelect?.(item)} />
            <div class="flex items-center gap-0.5">
              <ThemeSwitch value={theme} variant="icon-only" onValueChange={onThemeChange} />
              <NotificationsPopover>
                {#snippet children({ props, unreadCount })}
                  <Button
                    {...props}
                    variant="ghost"
                    size="icon"
                    class="text-muted-foreground hover:text-foreground relative size-8 rounded-lg"
                    aria-label="Notifications"
                  >
                    <Bell class="size-4" />
                    {#if unreadCount > 0}
                      <span
                        class="bg-primary ring-background absolute top-1.5 right-1.5 size-2 rounded-full ring-2"
                      ></span>
                    {/if}
                  </Button>
                {/snippet}
              </NotificationsPopover>
              <!-- ProfileMenu removed from header: the sidebar's NavUser
                   (sidebar-02 footer) already owns the profile/account
                   affordance. Two avatars in the same screen pulled the
                   eye in conflicting directions. The block still accepts
                   a `user` prop + `onProfileSelect` so consumers can
                   wire those into NavUser or a custom header slot. -->
            </div>
          </div>
        </header>
        <main id="main-content" tabindex="-1" class="flex flex-1 flex-col px-4 pt-4 pb-4 outline-none lg:px-6 lg:pt-6 lg:pb-6">
          {@render children?.()}
        </main>
      </SidebarInset>
    </SidebarProvider>
    

Raw manifest:https://uipkge.dev/r/svelte/dashboard-layout.json