Sidebar
sidebaruiFull-height app sidebar — collapsible to icons, with grouping, sub-grouping, and integrated search. The navigation surface for product apps with many sections.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar.jsonManifest:
https://uipkge.dev/r/svelte/sidebar.jsonInstalls to:src/lib/components/ui/sidebar/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'default''outline' | default | optional |
size | 'default''sm''lg' | default | optional |
side | 'left' | 'right' | left | optional |
collapsible | 'offcanvas' | 'icon' | 'none' | offcanvas | optional |
ref | HTMLDivElement | null | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
SidebarContextValueinterface SidebarContextValue {
readonly state: 'expanded' | 'collapsed'
readonly open: boolean
setOpen: (value: boolean) => void
readonly isMobile: boolean
readonly openMobile: boolean
setOpenMobile: (value: boolean) => void
toggleSidebar: () => void
}npm dependencies
Used by
Files installed (27)
src/lib/components/ui/sidebar/Sidebar.svelte4.1 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarProps extends HTMLAttributes<HTMLDivElement> { side?: 'left' | 'right' variant?: 'sidebar' | 'floating' | 'inset' collapsible?: 'offcanvas' | 'icon' | 'none' ref?: HTMLDivElement | null } </script> <script lang="ts"> import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '$lib/components/ui/sheet' import { cn } from '$lib/utils' import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils' let { class: className, side = 'left', variant = 'sidebar', collapsible = 'offcanvas', children, ref = $bindable(null), ...restProps }: SidebarProps = $props() const sidebar = useSidebar() </script> {#if collapsible === 'none'} <div bind:this={ref} data-uipkge="" data-slot="sidebar" class={cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', className)} {...restProps} > {@render children?.()} </div> {:else if sidebar.isMobile} <Sheet open={sidebar.openMobile} onOpenChange={sidebar.setOpenMobile}> <SheetContent data-sidebar="sidebar" data-uipkge="" data-slot="sidebar" data-mobile="true" {side} class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" style="--sidebar-width:{SIDEBAR_WIDTH_MOBILE}" {...restProps} > <SheetHeader class="sr-only"> <SheetTitle>Sidebar</SheetTitle> <SheetDescription>Displays the mobile sidebar.</SheetDescription> </SheetHeader> <div class="flex h-full w-full flex-col"> {@render children?.()} </div> </SheetContent> </Sheet> {:else} <div bind:this={ref} class="group peer text-sidebar-foreground hidden md:block" data-uipkge="" data-slot="sidebar" data-state={sidebar.state} data-collapsible={sidebar.state === 'collapsed' ? collapsible : ''} data-variant={variant} data-side={side} > <!-- This is what handles the sidebar gap on desktop --> <div class={cn( 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear', 'group-data-[collapsible=offcanvas]:w-0', 'group-data-[side=right]:rotate-180', variant === 'floating' || variant === 'inset' ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)', )} ></div> <div class={cn( 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex', side === 'left' ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', // Adjust the padding for floating and inset variants (token-aligned with React/shadcn). variant === 'floating' || variant === 'inset' ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]' : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l', className, )} {...restProps} > <div data-sidebar="sidebar" class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" > {@render children?.()} </div> </div> </div> {/if} <style> /* Push any OverlayScroll thumb inside a sidebar 2.5rem in from the right edge so it clears SidebarRail's inside-zone. The CSS variable cascades to every descendant, so consumers wrapping their nav in OverlayScroll (a common pattern when nav items overflow viewport height) get this for free -- no per-call thumb-offset config. */ [data-slot='sidebar'] { --ovs-thumb-right: 2.5rem; } </style>src/lib/components/ui/sidebar/SidebarContent.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarContentProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarContentProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-content" data-sidebar="content" class={cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/sidebar/SidebarFooter.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarFooterProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarFooterProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-footer" data-sidebar="footer" class={cn('flex flex-col gap-2 p-2', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/sidebar/SidebarGroup.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarGroupProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarGroupProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-group" data-sidebar="group" class={cn('relative flex w-full min-w-0 flex-col p-2', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/sidebar/SidebarGroupAction.svelte1.3 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements' export interface SidebarGroupActionProps extends HTMLButtonAttributes { /** Render your own element — spread `props` onto it. */ child?: Snippet<[{ props: Record<string, unknown> }]> ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, child, children, ref = $bindable(null), ...restProps }: SidebarGroupActionProps = $props() const mergedProps = $derived({ 'data-uipkge': '', 'data-slot': 'sidebar-group-action', 'data-sidebar': 'group-action', class: cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'after:absolute after:-inset-2 md:after:hidden', 'group-data-[collapsible=icon]:hidden', className, ), ...restProps, }) </script> {#if child} {@render child({ props: mergedProps })} {:else} <button bind:this={ref} type="button" {...mergedProps}>{@render children?.()}</button> {/if}src/lib/components/ui/sidebar/SidebarGroupContent.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarGroupContentProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarGroupContentProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-group-content" data-sidebar="group-content" class={cn('w-full text-sm', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/sidebar/SidebarGroupLabel.svelte1.2 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface SidebarGroupLabelProps extends HTMLAttributes<HTMLDivElement> { /** Render your own element — spread `props` onto it. */ child?: Snippet<[{ props: Record<string, unknown> }]> ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, child, children, ref = $bindable(null), ...restProps }: SidebarGroupLabelProps = $props() const mergedProps = $derived({ 'data-uipkge': '', 'data-slot': 'sidebar-group-label', 'data-sidebar': 'group-label', class: cn( 'text-sidebar-foreground/70 ring-sidebar-ring mt-2 mb-1 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', className, ), ...restProps, }) </script> {#if child} {@render child({ props: mergedProps })} {:else} <div bind:this={ref} {...mergedProps}>{@render children?.()}</div> {/if}src/lib/components/ui/sidebar/SidebarHeader.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarHeaderProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarHeaderProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-header" data-sidebar="header" class={cn('flex flex-col gap-2 p-2', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/sidebar/SidebarInput.svelte0.9 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { InputProps } from '$lib/components/ui/input' // Extends InputProps (not raw HTML attrs): the rest-spread below must match // Input exactly — Input redefines `size`/`prefix`, so raw attrs are not assignable. export interface SidebarInputProps extends InputProps { children?: Snippet } </script> <script lang="ts"> import { Input } from '$lib/components/ui/input' import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarInputProps = $props() </script> <!-- Sidebar-tuned wrapper over the kit's <Input> with sidebar height + bg defaults. --> <Input data-uipkge="" data-slot="sidebar-input" data-sidebar="input" bind:ref class={cn('bg-background h-8 w-full shadow-none', className)} {...restProps} > {@render children?.()} </Input>src/lib/components/ui/sidebar/SidebarInset.svelte1 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarInsetProps extends HTMLAttributes<HTMLElement> { ref?: HTMLElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarInsetProps = $props() </script> <main bind:this={ref} data-uipkge="" data-slot="sidebar-inset" class={cn( // min-w-0 is load-bearing: a flex-1 child without it inherits min-width: auto, // which means any single wide descendant (chart, table, code block) blows the // inset's width past its grid track. The upstream shadcn-ui sidebar omits this. 'bg-background relative flex w-full min-w-0 flex-1 flex-col', 'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2', className, )} {...restProps} > {@render children?.()} </main>src/lib/components/ui/sidebar/SidebarMenu.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarMenuProps extends HTMLAttributes<HTMLUListElement> { ref?: HTMLUListElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarMenuProps = $props() </script> <ul bind:this={ref} data-uipkge="" data-slot="sidebar-menu" data-sidebar="menu" class={cn('flex w-full min-w-0 flex-col gap-1', className)} {...restProps} > {@render children?.()} </ul>src/lib/components/ui/sidebar/SidebarMenuAction.svelte1.7 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements' export interface SidebarMenuActionProps extends HTMLButtonAttributes { showOnHover?: boolean /** Render your own element — spread `props` onto it. */ child?: Snippet<[{ props: Record<string, unknown> }]> ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, showOnHover = false, child, children, ref = $bindable(null), ...restProps }: SidebarMenuActionProps = $props() const mergedProps = $derived({ 'data-uipkge': '', 'data-slot': 'sidebar-menu-action', 'data-sidebar': 'menu-action', class: cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', 'after:absolute after:-inset-2 md:after:hidden', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', 'peer-data-[size=lg]/menu-button:top-2.5', 'group-data-[collapsible=icon]:hidden', showOnHover && 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0', className, ), ...restProps, }) </script> {#if child} {@render child({ props: mergedProps })} {:else} <button bind:this={ref} type="button" {...mergedProps}>{@render children?.()}</button> {/if}src/lib/components/ui/sidebar/SidebarMenuBadge.svelte1 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarMenuBadgeProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarMenuBadgeProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-menu-badge" data-sidebar="menu-badge" class={cn( 'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none', 'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', 'peer-data-[size=lg]/menu-button:top-2', 'group-data-[collapsible=icon]:hidden', className, )} {...restProps} > {@render children?.()} </div>src/lib/components/ui/sidebar/SidebarMenuButton.svelte2 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements' export interface SidebarMenuButtonProps extends HTMLButtonAttributes { variant?: 'default' | 'outline' size?: 'default' | 'sm' | 'lg' isActive?: boolean /** Tooltip shown when the sidebar is collapsed to icons. */ tooltip?: string | Snippet /** Render your own element — spread `props` onto it. */ child?: Snippet<[{ props: Record<string, unknown> }]> ref?: HTMLElement | null } </script> <script lang="ts"> import { Tooltip, TooltipContent, TooltipTrigger } from '$lib/components/ui/tooltip' import { useSidebar } from './utils' import SidebarMenuButtonChild from './SidebarMenuButtonChild.svelte' let { class: className, variant = 'default', size = 'default', isActive, tooltip, child, children, ref = $bindable(null), ...restProps }: SidebarMenuButtonProps = $props() const sidebar = useSidebar() </script> {#if !tooltip} <SidebarMenuButtonChild {variant} {size} {isActive} {child} bind:ref class={className} {...restProps}> {@render children?.()} </SidebarMenuButtonChild> {:else} <!-- w-full: the Tooltip positioning wrapper must not shrink-wrap the full-width menu button (truncated sidebar labels). --> <Tooltip class="w-full"> <TooltipTrigger> {#snippet child({ props }: { props: Record<string, unknown> })} <SidebarMenuButtonChild {variant} {size} {isActive} {child} bind:ref class={className} {...restProps} {...props} > {@render children?.()} </SidebarMenuButtonChild> {/snippet} </TooltipTrigger> <TooltipContent side="right" align="center" hidden={sidebar.state !== 'collapsed' || sidebar.isMobile}> {#if typeof tooltip === 'string'} {tooltip} {:else} {@render tooltip()} {/if} </TooltipContent> </Tooltip> {/if}src/lib/components/ui/sidebar/SidebarMenuButtonChild.svelte1.2 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements' export interface SidebarMenuButtonChildProps extends HTMLButtonAttributes { variant?: 'default' | 'outline' size?: 'default' | 'sm' | 'lg' isActive?: boolean /** Render your own element — spread `props` onto it. */ child?: Snippet<[{ props: Record<string, unknown> }]> ref?: HTMLElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { sidebarMenuButtonVariants } from './sidebar.variants' let { class: className, variant = 'default', size = 'default', isActive, child, children, ref = $bindable(null), ...restProps }: SidebarMenuButtonChildProps = $props() const mergedProps = $derived({ 'data-uipkge': '', 'data-slot': 'sidebar-menu-button', 'data-sidebar': 'menu-button', 'data-size': size, 'data-active': isActive, class: cn(sidebarMenuButtonVariants({ variant, size }), className), ...restProps, }) </script> {#if child} {@render child({ props: mergedProps })} {:else} <button bind:this={ref as HTMLButtonElement | null} type="button" {...mergedProps}> {@render children?.()} </button> {/if}src/lib/components/ui/sidebar/SidebarMenuItem.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarMenuItemProps extends HTMLAttributes<HTMLLIElement> { ref?: HTMLLIElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarMenuItemProps = $props() </script> <li bind:this={ref} data-uipkge="" data-slot="sidebar-menu-item" data-sidebar="menu-item" class={cn('group/menu-item relative', className)} {...restProps} > {@render children?.()} </li>src/lib/components/ui/sidebar/SidebarMenuSkeleton.svelte0.9 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarMenuSkeletonProps extends HTMLAttributes<HTMLDivElement> { showIcon?: boolean width?: string ref?: HTMLDivElement | null } </script> <script lang="ts"> import { Skeleton } from '$lib/components/ui/skeleton' import { cn } from '$lib/utils' let { class: className, showIcon = false, width = '70%', ref = $bindable(null), ...restProps }: SidebarMenuSkeletonProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" class={cn('flex h-8 items-center gap-2 rounded-md px-2', className)} {...restProps} > {#if showIcon} <Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" /> {/if} <Skeleton class="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" style="--skeleton-width:{width}" /> </div>src/lib/components/ui/sidebar/SidebarMenuSub.svelte0.7 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarMenuSubProps extends HTMLAttributes<HTMLUListElement> { ref?: HTMLUListElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarMenuSubProps = $props() </script> <ul bind:this={ref} data-uipkge="" data-slot="sidebar-menu-sub" data-sidebar="menu-badge" class={cn( 'border-sidebar-border mx-3 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2 py-0.5', 'group-data-[collapsible=icon]:hidden', className, )} {...restProps} > {@render children?.()} </ul>src/lib/components/ui/sidebar/SidebarMenuSubButton.svelte1.7 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAnchorAttributes } from 'svelte/elements' export interface SidebarMenuSubButtonProps extends HTMLAnchorAttributes { size?: 'sm' | 'md' isActive?: boolean /** Render your own element — spread `props` onto it. */ child?: Snippet<[{ props: Record<string, unknown> }]> ref?: HTMLAnchorElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, size = 'md', isActive, child, children, ref = $bindable(null), ...restProps }: SidebarMenuSubButtonProps = $props() const mergedProps = $derived({ 'data-uipkge': '', 'data-slot': 'sidebar-menu-sub-button', 'data-sidebar': 'menu-sub-button', 'data-size': size, 'data-active': isActive, class: cn( 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', size === 'sm' && 'text-xs', size === 'md' && 'text-sm', 'group-data-[collapsible=icon]:hidden', className, ), ...restProps, }) </script> {#if child} {@render child({ props: mergedProps })} {:else} <a bind:this={ref} {...mergedProps}>{@render children?.()}</a> {/if}src/lib/components/ui/sidebar/SidebarMenuSubItem.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarMenuSubItemProps extends HTMLAttributes<HTMLLIElement> { ref?: HTMLLIElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarMenuSubItemProps = $props() </script> <li bind:this={ref} data-uipkge="" data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" class={cn('group/menu-sub-item relative', className)} {...restProps} > {@render children?.()} </li>src/lib/components/ui/sidebar/SidebarProvider.svelte3.8 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarProviderProps extends HTMLAttributes<HTMLDivElement> { /** Uncontrolled initial open state (defaults to true). Use this when you do not need to bind or control open. */ defaultOpen?: boolean /** Controlled open state. Bind with `bind:open` for two-way binding or pass `defaultOpen` for initial-only. */ open?: boolean onOpenChange?: (open: boolean) => void ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { setSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON, } from './utils' let { class: className, defaultOpen = true, open = $bindable(defaultOpen), onOpenChange, children, ref = $bindable(null), ...restProps }: SidebarProviderProps = $props() // `matchMedia` doesn't exist during SSR. Gating on `mounted` makes both the // server and the client's first synchronous render produce the desktop // branch unconditionally; `onMount` then flips the flag and `Sidebar`'s // mobile branch re-runs against the real matchMedia signal. Cost: a brief // desktop-layout flash for mobile users on first paint. That's the // universal tradeoff for SSR-without-viewport-detection. let mounted: boolean = $state(false) let mobileQuery: boolean = $state(false) let openMobile: boolean = $state(false) $effect(() => { mounted = true if (typeof window === 'undefined' || !('matchMedia' in window)) return const mq = window.matchMedia('(max-width: 768px)') const sync = () => (mobileQuery = mq.matches) sync() mq.addEventListener('change', sync) const onKeyDown = (event: KeyboardEvent) => { if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { event.preventDefault() toggleSidebar() } } document.addEventListener('keydown', onKeyDown) return () => { mq.removeEventListener('change', sync) document.removeEventListener('keydown', onKeyDown) } }) const isMobile = $derived(mounted && mobileQuery) function setOpen(value: boolean) { open = value onOpenChange?.(value) if (typeof document !== 'undefined') { // This sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` } } function setOpenMobile(value: boolean) { openMobile = value } // Helper to toggle the sidebar. function toggleSidebar() { return isMobile ? setOpenMobile(!openMobile) : setOpen(!open) } // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. // Named `sidebarState`, not `state`: a `state` binding in this scope would // make the compiler read every `$state(...)` below as a store subscription. const sidebarState: 'expanded' | 'collapsed' = $derived(open ? 'expanded' : 'collapsed') // Getters over $state keep every consumer reactive without runes in utils.ts. // Consumers must read via `sidebar.open` (not destructured) to stay subscribed. setSidebarContext({ get state() { return sidebarState }, get open() { return open }, setOpen, get isMobile() { return isMobile }, get openMobile() { return openMobile }, setOpenMobile, toggleSidebar, }) </script> <div bind:this={ref} data-uipkge="" data-slot="sidebar-wrapper" style="--sidebar-width:{SIDEBAR_WIDTH};--sidebar-width-icon:{SIDEBAR_WIDTH_ICON}" class={cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/sidebar/SidebarRail.svelte1.7 kB
<script lang="ts" module> import type { HTMLButtonAttributes } from 'svelte/elements' export interface SidebarRailProps extends HTMLButtonAttributes { ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { useSidebar } from './utils' let { class: className, children, ref = $bindable(null), onclick, ...restProps }: SidebarRailProps = $props() const sidebar = useSidebar() function handleClick(e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) { sidebar.toggleSidebar() onclick?.(e) } </script> <button bind:this={ref} type="button" data-sidebar="rail" data-uipkge="" data-slot="sidebar-rail" aria-label="Toggle Sidebar" tabindex={-1} title="Toggle Sidebar" class={cn( 'hover:after:bg-sidebar-border focus-visible:ring-ring absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-colors duration-200 ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-px focus-visible:ring-2 focus-visible:outline-none sm:flex', 'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize', '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize', 'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full', '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2', '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2', className, )} onclick={handleClick} {...restProps} > {@render children?.()} </button>src/lib/components/ui/sidebar/SidebarSeparator.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarSeparatorProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { Separator } from '$lib/components/ui/separator' import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: SidebarSeparatorProps = $props() </script> <Separator data-uipkge="" data-slot="sidebar-separator" data-sidebar="separator" bind:ref class={cn('bg-sidebar-border mx-2 w-auto', className)} {...restProps} > {@render children?.()} </Separator>src/lib/components/ui/sidebar/SidebarTrigger.svelte1.1 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface SidebarTriggerProps extends HTMLAttributes<HTMLButtonElement> { ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { PanelLeft } from '@lucide/svelte' import { Button } from '$lib/components/ui/button' import { cn } from '$lib/utils' import { useSidebar } from './utils' let { class: className, children, ref = $bindable(null), onclick, ...restProps }: SidebarTriggerProps = $props() const sidebar = useSidebar() function handleClick(e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) { sidebar.toggleSidebar() onclick?.(e) } </script> <Button data-sidebar="trigger" data-uipkge="" data-slot="sidebar-trigger" variant="ghost" size="icon" bind:ref class={cn('focus-visible:ring-ring h-7 w-7 focus-visible:ring-2 focus-visible:outline-none', className)} onclick={handleClick} {...restProps} > <PanelLeft /> <span class="sr-only">Toggle Sidebar</span> {@render children?.()} </Button>src/lib/components/ui/sidebar/sidebar.variants.ts1.9 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so consuming Vue SFCs can import without creating a circular * dependency through the index. See card.variants.ts for the canonical * example + the SSR symptom that motivated the split. */ export const sidebarMenuButtonVariants = cva( 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm text-sidebar-foreground/80 outline-hidden ring-sidebar-ring transition-[width,height,padding,color,background-color] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-foreground/70', { variants: { variant: { default: 'hover:bg-sidebar-accent hover:text-foreground', outline: 'bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]', }, size: { default: 'h-8 text-sm', sm: 'h-7 text-xs', lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>src/lib/components/ui/sidebar/index.ts2.4 kB
export { default as Sidebar, type SidebarProps } from './Sidebar.svelte' export { default as SidebarContent, type SidebarContentProps } from './SidebarContent.svelte' export { default as SidebarFooter, type SidebarFooterProps } from './SidebarFooter.svelte' export { default as SidebarGroup, type SidebarGroupProps } from './SidebarGroup.svelte' export { default as SidebarGroupAction, type SidebarGroupActionProps } from './SidebarGroupAction.svelte' export { default as SidebarGroupContent, type SidebarGroupContentProps } from './SidebarGroupContent.svelte' export { default as SidebarGroupLabel, type SidebarGroupLabelProps } from './SidebarGroupLabel.svelte' export { default as SidebarHeader, type SidebarHeaderProps } from './SidebarHeader.svelte' export { default as SidebarInput, type SidebarInputProps } from './SidebarInput.svelte' export { default as SidebarInset, type SidebarInsetProps } from './SidebarInset.svelte' export { default as SidebarMenu, type SidebarMenuProps } from './SidebarMenu.svelte' export { default as SidebarMenuAction, type SidebarMenuActionProps } from './SidebarMenuAction.svelte' export { default as SidebarMenuBadge, type SidebarMenuBadgeProps } from './SidebarMenuBadge.svelte' export { default as SidebarMenuButton, type SidebarMenuButtonProps } from './SidebarMenuButton.svelte' export { default as SidebarMenuItem, type SidebarMenuItemProps } from './SidebarMenuItem.svelte' export { default as SidebarMenuSkeleton, type SidebarMenuSkeletonProps } from './SidebarMenuSkeleton.svelte' export { default as SidebarMenuSub, type SidebarMenuSubProps } from './SidebarMenuSub.svelte' export { default as SidebarMenuSubButton, type SidebarMenuSubButtonProps } from './SidebarMenuSubButton.svelte' export { default as SidebarMenuSubItem, type SidebarMenuSubItemProps } from './SidebarMenuSubItem.svelte' export { default as SidebarProvider, type SidebarProviderProps } from './SidebarProvider.svelte' export { default as SidebarRail, type SidebarRailProps } from './SidebarRail.svelte' export { default as SidebarSeparator, type SidebarSeparatorProps } from './SidebarSeparator.svelte' export { default as SidebarTrigger, type SidebarTriggerProps } from './SidebarTrigger.svelte' export { useSidebar } from './utils' // Re-export variant API from the sibling file (kept separate to avoid the // Component.svelte <-> index.ts circular import that broke dev SSR for Card). export { sidebarMenuButtonVariants, type SidebarMenuButtonVariants } from './sidebar.variants'src/lib/components/ui/sidebar/utils.ts0.9 kB
import { getContext, setContext } from 'svelte' export const SIDEBAR_COOKIE_NAME = 'sidebar_state' export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 export const SIDEBAR_WIDTH = '16rem' export const SIDEBAR_WIDTH_MOBILE = '18rem' export const SIDEBAR_WIDTH_ICON = '3rem' export const SIDEBAR_KEYBOARD_SHORTCUT = 'b' const SIDEBAR_CONTEXT_KEY = Symbol('uipkge-sidebar') export interface SidebarContextValue { readonly state: 'expanded' | 'collapsed' readonly open: boolean setOpen: (value: boolean) => void readonly isMobile: boolean readonly openMobile: boolean setOpenMobile: (value: boolean) => void toggleSidebar: () => void } export function setSidebarContext(value: SidebarContextValue) { setContext(SIDEBAR_CONTEXT_KEY, value) } export function useSidebar(): SidebarContextValue { return getContext<SidebarContextValue>(SIDEBAR_CONTEXT_KEY) }
Raw manifest:https://uipkge.dev/r/svelte/sidebar.json