Dashboard Layout
blocklayoutFull 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
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/dashboard-layout.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/dashboard-layout.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/dashboard-layout.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/dashboard-layout.jsonManifest:
https://uipkge.dev/r/svelte/dashboard-layout.jsonInstalls to:src/lib/components/blocks/dashboard-layout/Variants
Loading interactive previews…
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