Context
shadcn/uiλ₯Ό νμ©ν΄ Sidebar μ»΄ν¬λνΈλ₯Ό ꡬννκ² λμλ€. Sidebar μ»΄ν¬λνΈλ₯Ό λ€μ΄λ‘λ λ°μ ν μ΄ν΄λ³΄λ λ΄μ©μ΄ λ§€μ° λ§κ³ ν λμ μ΄ν΄νκΈ°μ νκ³κ° μμλ€. λ°λΌμ, λ‘컬μ μλ μ½λλ₯Ό μ΄ν΄λ³΄λ©΄μ λ΄μ©μ μ 리 ν΄λ³΄κΈ° μν΄ μ΄ κΈμ μμ±νκ² λμλ€.
Agenda
β’
shadcn/uiμ Sidebar μ»΄ν¬λνΈμ λΆκ° μμλ₯Ό μ΄ν΄λ³΄κ³ μ½λλ₯Ό λΆμ λ° μ΄ν΄
Contents
μλ shadcn/uiμμ μ 곡νλ Sidebar μ»΄ν¬λνΈμ ꡬ쑰μ΄λ€.
μλμ λͺ
λ Ήμ΄λ₯Ό ν΅ν΄ Sidebar μ»΄ν¬λνΈλ₯Ό λ€μ΄λ‘λνλ, μ¬λ¬ νμΌμ΄ λ ν¬μ§ν 리μ λ€μ΄λ‘λ λμλ€.
$ pnpm dlx shadcn@latest add sidebar
Shell
볡μ¬
λ€μ΄λ‘λ λ νμΌμ μ¬λ¬κ°λ‘ Sidebar μ»΄ν¬λνΈμμ μ¬μ© λλ μ»΄ν¬λνΈμ λΆμμ μΌλ‘ μ¬μ©ν μ μλ μ»΄ν¬λνΈλ€μ΄μλ€.
곡μ λ¬Έμμμ μλ €μ€ λ°©λ²λλ‘ μλμ νμΌμ ꡬμ±νλ€.
β’
app-sidebar.tsx
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
} from "@/components/ui/sidebar"
const AppSidebar = () => {
return (
<Sidebar>
<SidebarHeader />
<SidebarContent>
<SidebarGroup />
<SidebarGroup />
</SidebarContent>
<SidebarFooter />
</Sidebar>
);
};
export default AppSidebar;
JavaScript
볡μ¬
β’
layout.tsx
import { Outlet } from 'react-router-dom';
import AppSidebar from '@/components/app-sidebar';
import { SidebarProvider, SidebarTrigger } from '@/ui/components/sidebar';
const DevLayout = () => {
return (
<SidebarProvider>
<AppSidebar />
<SidebarTrigger />
<Outlet />
</SidebarProvider>
);
};
export default DevLayout;
Shell
볡μ¬
layout.tsxλ routerλ₯Ό ν΅ν΄ νμ νμ΄μ§λ₯Ό ννν μμ μ΄λΌ <Outlet />μ μ¬μ©νλ€.
컨ν
μΈ λ₯Ό νμνλ Layoutμ SidebarProviderλ‘λΆν° OutletκΉμ§μ depthλ₯Ό μ΄λ£¨κ³ μλ€. Sidebar μ»΄ν¬λνΈ νμΌμ μ½ 700μ€μ λ¬νλ λ§€μ° κΈ΄ νμΌμΈλ°, SidebarProviderκ° μ¬μ΄λλ°μ μν, λͺ¨λ°μΌ λΆκΈ°, ν κΈ λ±μ λͺ¨λ κ²°μ νλ 컨νΈλ‘€ νμ μν μ νκ³ μλ κ² κ°μ SidebarProviderλΆν° μ΄ν΄λ³΄κΈ°λ‘ νλ€.
SidebarProvider
SidebarProviderμ μ€νλ¬Έ 첫 μ€ λΆν° μλμ λΆλΆμ΄ λμ λλ€.
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
JavaScript
볡μ¬
κ°μΈμ μΌλ‘ μΉ νλ‘μ νΈλ₯Ό μ§ννλ©΄μ κ°μ₯ κΉλ€λ‘κ³ μ΄λ €μ΄ λΆλΆμ΄ Mobile λΆκΈ° λ° λμμ΄λΌκ³ μκ°νλλ°, shadcnμ useIsMobile ν
μ ν΅ν΄ λͺ¨λ°μΌ λΆκΈ°λ₯Ό μ²λ¦¬νλ€.
import { useEffect, useState } from 'react';
const MOBILE_BREAKPOINT = 768;
export const useIsMobile = () => {
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
};
JavaScript
볡μ¬
λ΄μ©μ μμ κ°λ€. κΈ°μ‘΄ μ½λλ export function useIsMobile()λ‘ μ μΈλμ΄μμ§λ§, arrow functionμ μ¬μ©νλ λ°©ν₯μ΄ λ§μμ λ€μ΄ μμ νμλ€. μμΌλ‘μ μ½λ λν arrow functionμ μ¬μ©νλ€.
const MOBILE_BREAKPOINT = 768;
JavaScript
볡μ¬
μ΄ ν
μ 768μ΄λΌλ κ°μ ν΅ν΄ λͺ¨λ°μΌ λΆκΈ°μ μ μ‘λλ€. λλΉ 768 λ―Έλ§μ λͺ¨λ°μΌ, λλΉ 768 μ΄μμ λ°μ€ν¬νμΌλ‘ λͺ¨λ°μΌνλ©΄μΈμ§ μλμ§λ₯Ό ꡬλΆνλ€.
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
JavaScript
볡μ¬
isMobileμ μν κ°μ μ μ₯νλ λ³μλ‘, μ΄κΈ° κ°μ undefinedλ‘ μ§μ λμ΄μλ€. νλ©΄μ΄ λ λλ§ λκΈ° μ μλ λͺ¨λ°μΌνλ©΄μΈμ§ μλμ§ νλ¨ν μ μκΈ° λλ¬Έμ undefinedλ‘ μ§μ λμ΄μλ€.
useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
JavaScript
볡μ¬
useEffectν
μΌλ‘ νλ©΄μ λ λλ§μ΄ μλ£λ ν μ€νλλ€.
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
JavaScript
볡μ¬
window μ μ κ°μ²΄μ matchMedia APIλ₯Ό μ¬μ©νμ¬ νμ¬ νλ©΄μ λλΉκ° μ΅λ 767px μ΄νμ 쑰건μ λ§μ‘±νλμ§ μνλμ§ νλ¨νλ€. mqlμ Media Query Listμ μ½μλ‘ 768 λ―Έλ§μ 쑰건μ κ°μ§ matchMedia κ°μ²΄λ₯Ό κ°λλ€.
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
JavaScript
볡μ¬
onChange ν¨μλ isMobile μν κ°μ νμ¬ λλΉκ° λΆκΈ°μ λ³΄λ€ μμΌλ©΄ true κ°μΌλ‘, κ°κ±°λ ν¬λ©΄ false κ°μΌλ‘ μ§μ νλ€. μ΄ onChange ν¨μλ mql κ°μ²΄μ change μ΄λ²€νΈμ λ±λ‘ λλ€.
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
JavaScript
볡μ¬
μ΄ μ€νλ¬Έμ νλ©΄μ΄ μ²μ λ λλ§μ΄ μλ£λ ν μ€νλλ ꡬ문μ΄λ€. μ²μ isMobile κ°μ undefined κ°μΌλ‘ λͺ¨λ°μΌμΈμ§ μλμ§ νλ¨ν μ μλ€. λ°λΌμ μ΅μ΄ νλ©΄ λ λλ§ ν νμ¬ νλ©΄ λλΉμ λΆκΈ°κ°μ ν¬κΈ°λ₯Ό λΉκ΅νμ¬ κ°μ ν λΉνλ€.
return () => mql.removeEventListener('change', onChange);
JavaScript
볡μ¬
cleanup ν¨μλ‘ μ»΄ν¬λνΈκ° μΈλ§μ΄νΈ λκ±°λ useEffectκ° λ€μ μ€νλκΈ° μ§μ μ μ€ν λλ€. λ©λͺ¨λ¦¬ λμμ μ€λ³΅ μ€νμ λ§κΈ° μν΄ μμ± λμλ€.
return !!isMobile;
JavaScript
볡μ¬
μ€μν λΆλΆμΌλ‘ νμ¬ Mobile νλ©΄μΈμ§ μλμ§ νλ¨νλ μν κ°μ λ°ννλ€.
λ€μ Sidebar μ»΄ν¬λνΈμ SidebarProviderλ‘ λμμμ,
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === 'function' ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
JavaScript
볡μ¬
μμ μ½λλ Sidebarμ μ΄λ¦Ό/λ«ν μνλ₯Ό κ΄λ¦¬νλ λΆλΆμ΄λ€. SidebarProviderλ μΈλΆμμ Sidebarμ μ΄λ¦Ό λ«νμ μ΅μ΄ μνλ₯Ό κ²°μ μ§μ μ μκ³ μ»¨νΈλ‘€ λν κ°λ₯νκ²λ μ€κ³ λμ΄μλ€.
β’
defaultOpenμ SidebarProviderμ νλΌλ―Έν°λ‘ κΈ°λ³Έ μ΄λ¦Ό/λ«ν μνλ₯Ό μ§μ ν μ μλ€.
β¦
trueλ©΄ μ΄λ¦Ό, falseλ©΄ λ«ν μνμ΄λ€.
β’
openPropμ SidebarProviderμ νλΌλ―Έν°λ‘ Sidebarμ μ΄λ¦Ό/λ«ν μνλ₯Ό μ§μ νλ κ°μ΄λ€. openPropμ openμ asliasμΈλ°, defaultOpen κ°μ΄ μ€μ λμ΄μμ΄λ openProp κ°μ΄ νλΌλ―Έν°λ‘ μ£Όμ΄μ§λ©΄ openProp κ°μ λ°λΌκ°λ€.
β’
setOpenμ Sidebarμ λ΄λΆ μ΄λ¦Ό/λ«ν μνλ₯Ό κ΄λ¦¬νλ ν¨μμ΄λ€. useCallback νλΌλ―Έν°λ‘ boolean | (value: boolean) => boolean λ νμ
μ νλΌλ―Έν°λ₯Ό νμ©νλλ° μ΄λ setOpen(true), setOpen(false), setOpen(prev => !prev) μ κ°μ νν λͺ¨λ μ§μνκΈ° μν¨μΈ κ²μΌλ‘ μΆμΈ‘νλ€. μΈλΆμμ νλΌλ―Έν°λ‘ μ§μ ν setOpenProp ν¨μκ° μμΌλ©΄ κ·Έ ν¨μλ₯Ό μ¬μ©, μμΌλ©΄ λ΄λΆμ μλ setterλ₯Ό μ¬μ©νλ€.
β’
μ κΈ°ν λΆλΆμ Cookieλ₯Ό μ¬μ©νμ¬ μ΄λ¦Ό/λ«ν μνλ₯Ό μ μ₯νλ κ²μΈλ°, μλ§ SSRμ μν΄ μμ±ν΄λ κ² μλκΉ μκ°νλ€. Next.jsμ κ°μ νλ μμν¬λ₯Ό μ¬μ©νμ¬ SSRμ μ¬μ©νμ§ μμκ±°λΌλ©΄ local-storageμ μ μ₯ν΄λ μκ΄ μμ κ²μΌλ‘ μμνλ€.
μλλ λ³κ²½ν μ½λ.
...
const SIDEBAR_STORAGE_NAME = 'sidebar_state';
...
// Internal state of the sidebar.
// Use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = useState(defaultOpen);
const open = openProp ?? _open;
// Enable setOpen(true/false) and setOpen(prev => !prev) patterns.
const setOpen = useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === 'function' ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// Save sidebar state in localStorage for persistence.
localStorage.setItem(SIDEBAR_STORAGE_NAME, String(openState));
},
[setOpenProp, open],
);
JavaScript
볡μ¬
μλμ μ½λλ Sidebarμ μ΄λ¦Ό/λ«ν μνλ₯Ό μ‘°μνλ μ½λμ΄λ€.
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);
JavaScript
볡μ¬
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
JavaScript
볡μ¬
toggleSidebar ν¨μλ Mobile νκ²½μ΄λ©΄ setOpenMobileμ μ¬μ©νμ¬, λ°μ€ν¬ν νκ²½μ΄λ©΄ setOpenμ μ¬μ©νμ¬ Sidebarμ μ΄λ¦Ό/λ«ν μνλ₯Ό μ§μ νλ€. useCallback ν¨μλ‘, ν¨μλ₯Ό λ§€λ² μλ‘ μμ±νμ§ μκ³ μμ‘΄μ± λ°°μ΄ μμ μλ νλͺ©λ€μ΄ λ³κ²½ λμμ κ²½μ°μ λ€μ ν¨μλ₯Ό κ·Έλ €λΈλ€.
...
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
...
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);
JavaScript
볡μ¬
μ΄ hookμ λ¨μΆν€λ₯Ό μ¬μ©νμ¬ μ¬μ΄λλ°λ₯Ό μ΄κ³ λ«μ μ μκ² μ€μ νλ λΆλΆμ΄λ€. 컨νΈλ‘€ + SIDEBAR_KEYBOARD_SHORTCUTμ λλ₯΄λ©΄ Sidebarμ μ΄λ¦Ό/λ«ν μνκ° μ νλλ€.
// 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.
const state = open ? 'expanded' : 'collapsed';
JavaScript
볡μ¬
boolean νμ
μΈ open κ°μ λ°λΌμ stateκ° expanded λλ collapsedλ‘ μ€μ λλ€. HTML Elementλ₯Ό μ΄ν΄λ³΄λ©΄ Sidebarμ data-state="expandedβ νμμΌλ‘ μμ±μ΄ μ€μ λμ΄μλ κ²μ λ³Ό μ μλ€. μ΄λ₯Ό ν΅ν΄ Tailwindμμ μλμ κ°μ΄ μ½κ² μ€νμΌλ§μ μ§νν μ μλ€.
className="data-[state=collapsed]:w-14 data-[state=expanded]:w-64"
JavaScript
볡μ¬
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
JavaScript
볡μ¬
Contextμ λκ²¨μ€ κ³΅μ© μν λ¬Άμμ λ§λλ μ½λμ΄λ€. μ΄λ₯Ό ν΅ν΄ Contextλ₯Ό μ°λ μ»΄ν¬λνΈλ μλμ κ°μ΄ Providerμμ λ겨주λ κ°μ μ¬μ©ν μ μλ€.
const {
state,
open,
setOpen,
isMobile,
openMobile,
toggleSidebar,
} = useSidebar();
JavaScript
볡μ¬
μλλ νμ¬κΉμ§ λ³ν λ μ½λ.
// Toggle function for sidebar.
// If mobile, toggle mobile sidebar state.
// If not mobile, toggle desktop sidebar state.
const toggleSidebar = useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpenMobile, setOpen]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
toggleSidebar();
}
};
// Add event listener for keyboard shortcut.
window.addEventListener('keydown', handleKeyDown);
// Cleanup event listener on unmount or when toggleSidebar changes.
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);
// Add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed';
const contextValue = useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
JavaScript
볡μ¬
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot='sidebar-wrapper'
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
className,
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
JavaScript
볡μ¬
Sidebar μ 체 λ μ΄μμκ³Ό μ μ ν΄ν μ€μ , 컨ν
μ€νΈ 곡κΈμ ν λ²μ κ°μΈλ ꡬ쑰μ΄λ€. SidebarProviderμ λ§μ§λ§ λΆλΆμ΄λ€. SidebarContext.Providerλ Sidebar κ΄λ ¨ μν/ν¨μλ₯Ό νμ μ»΄ν¬λνΈλ€μ΄ μ¬μ©ν μ μκ² νλ€. μ΄ Provider μμ μλ λͺ¨λ childrenμ Sidebarμ μνλ₯Ό 곡μ νκ² λλ€.
TooltipProviderλ Sidebarμ λ§μ°¬κ°μ§λ‘ Tooltipμ λμμ μ¬μ©ν μ μκ² νλ€. κ΄λ ¨ λ¬Έμ μ°Έκ³ .
<div
data-slot='sidebar-wrapper'
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
className,
)}
{...props}
>
{children}
</div>
JavaScript
볡μ¬
μ μ½λλ μ¬μ΄λλ°λ₯Ό ν¬ν¨ν λ μ΄μμμ κ°μΈλ wrapperμ΄λ€. data-slotμ μμμ λ§ν΄μλκ²μ²λΌ μ»΄ν¬λνΈ μλ³μ©μ΄λ€.
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
JavaScript
볡μ¬
κ°μΈμ μΌλ‘ μ κ²½μ°μλ λΆλΆμ μ΄ μ½λμ΄λ€. 컨벀μ
μμ μΈλΌμΈμ€νμΌμ μ°μ§ μκΈ°λ‘ νλλ°, style μμ±μ΄ λ±μ₯νμ¬ --sidebar-widthμ --sidebar-width-iconμ ν λΉνλ€. λ°λΌμ data-* μλ³μλ₯Ό νμ©ν΄ mobileμΈμ§ desktopΒ νλ©΄μΈμ§ ꡬλΆνμ¬ css νμΌμμ ν΄λΉ κ°μ λ³κ²½νλ λ°©μμΌλ‘ μμ νμλ€.
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot='sidebar-wrapper'
data-screen-type={isMobile ? 'mobile' : 'desktop'}
className={cn(
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full ',
className,
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
JavaScript
볡μ¬
/* desktop */
[data-slot="sidebar-wrapper"][data-screen-type="desktop"] {
--sidebar-width: 16rem;
--sidebar-width-icon: 3rem;
}
/* mobile */
[data-slot="sidebar-wrapper"][data-screen-type="mobile"] {
--sidebar-width: 18rem;
--sidebar-width-icon: 3rem;
}
CSS
볡μ¬
Sidebar
νλ©΄μ 보μ¬μ§λ Sidebar μ»΄ν¬λνΈμ΄λ€.
function Sidebar({
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
className,
children,
...props
}: React.ComponentProps<'div'> & {
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none';
}) {
...
}
JavaScript
볡μ¬
μμ μ½λλ Sidebar μ»΄ν¬λνΈκ° λ°μ μ μλ νλΌλ―Έν°μ κ·Έ νμ
μ΄λ€. side νλΌλ―Έν°λ Sidebarκ° μμΉν κ³³μ μ§μ νλ©°, left λλ rightλ‘ μ€μ ν μ μλ€. variantλ Sidebarκ° λ³΄μ΄λ ννμΈλ° sidebarλ λ°°κ²½μμ΄ μμΌλ©΄μ μμ ν μμ μ, μλ, μλ©΄μ΄ λΆμ΄μλ€. floatingμΌλ‘ μ€μ νλ©΄ paddingμ΄ μλ κ²μ²λΌ λνλλ©° insetμ λ°°κ²½μμ΄ μμ΄μ§κ³ κ²½κ³ λν μ¬λΌμ Έλ³΄μΈλ€. collapsible νλΌλ―Έν°λ offcanvas, icon, none μΈ κ°μ§ κ°μ λ°μ μ μλ€. offcanvasλ μ¬μ΄λλ°λ₯Ό μ κ³ νΌμΉ μ μμΌλ©° μ μμ λ μμ ν μ νμ μ¬μ΄λλ°κ° 보μ΄μ§ μλλ€. iconμΌλ‘ μ€μ μ μ κ³ νΌμΉ μ μκ³ μ μμ λ μμ΄μ½ μμμ΄ λ³΄μΈλ€. noneμΌλ‘ μ€μ μ μ μ μ μλ€.
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === 'none') {
return (
<div
data-slot='sidebar'
className={cn(
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
className,
)}
{...props}
>
{children}
</div>
);
}
JavaScript
볡μ¬
λ¨Όμ , useSidebar ν
μ ν΅ν΄ SidebarProviderμμ μ 곡νλ κ°μ μ¬μ©ν μ μλ€. κ·Έλ¦¬κ³ sidebarμ collapsible νλΌλ―Έν°κ° noneμΌ λ, sidebarλ μ μ μ μλ€. λ°λΌμ λΆκΈ°λ¬Έμ ν΅ν΄ κ³ μ λλΉμ 곡κ°μ λ§λ€μ΄ 컨ν
μΈ λ₯Ό νμνλ€.
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar='sidebar'
data-slot='sidebar'
data-mobile='true'
className='bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden'
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className='sr-only'>
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className='flex h-full w-full flex-col'>{children}</div>
</SheetContent>
</Sheet>
);
}
JavaScript
볡μ¬
MobileμΌ νλ©΄μΌ λ λ λλ§ λλ μ¬μ΄λλ°μ΄λ€. radix-uiμ wrapper μ±κ²©μ μ»΄ν¬λνΈλ₯Ό μ¬μ©νμ¬ Mobile μ¬μ΄λλ°λ₯Ό λΆκΈ°λ‘ μ²λ¦¬ν΄ νλ©΄μ λ λλ§ νλ€. νκ·Έ μμ style νκ·Έλ₯Ό μ¬μ©νλ κ²μ΄ μ«μ΄μ μλμ κ°μ΄ μμ νμλ€.
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar='sidebar'
data-slot='sidebar'
data-mobile='true'
className='bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden'
side={side}
>
<SheetHeader className='sr-only'>
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className='flex h-full w-full flex-col'>{children}</div>
</SheetContent>
</Sheet>
);
}
JavaScript
볡μ¬
CSS νμΌμ mobile λμμ νλ λ μΆκ°νλ€. [data-slot="sidebar"][data-mobile="true"] μ΄ λΆλΆμΈλ°, MobileμΌ λ Sheetμ Portalμ μ¬μ©νμ¬ κ΅¬μ‘°μ μΌλ‘ μΈλΆμ λ λλ§ λκΈ° λλ¬Έμ --sidebar-widthκ° μ μ© λμ§ μμμμ΄λ€.
/* desktop */
[data-slot="sidebar-wrapper"][data-screen-type="desktop"] {
--sidebar-width: 16rem;
--sidebar-width-icon: 3rem;
}
/* mobile */
[data-slot="sidebar-wrapper"][data-screen-type="mobile"],
[data-slot="sidebar"][data-mobile="true"] {
--sidebar-width: 18rem;
--sidebar-width-icon: 3rem;
}
CSS
볡μ¬
<div
className='group peer text-sidebar-foreground hidden md:block'
data-state={state}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
data-side={side}
data-slot='sidebar'
>
...
</div>
JavaScript
볡μ¬
Desktop νκ²½μμ λ λλ§ λλ μ¬μ΄λλ° μ»΄ν¬λνΈμ μμ λΆλΆμ΄λ€. classNameμ ν΅ν΄ μ¬μ΄λλ°μ μ€νμΌμ μ§μ νκ³ μλ€. groupμ μμ μμκ° group-* λ°©μμΌλ‘ λΆλͺ¨μ data-* κ°μ μ½μ΄ μ€νμΌλ§μ ν μ μκ² νλ€. peerλ groupκ³Ό λΉμ·νκ² νμ μ»΄ν¬λνΈκ° peer-* λ°©μμΌλ‘ μ΄ divμ data-* κ°μ μ½μ΄ μ€νμΌλ§μ ν μ μκ² νλ€. hidden md:blockμ λͺ¨λ°μΌμμλ μ¨κΈ°κ³ md μ¬μ΄μ¦ μ΄μμΌ λμ νλ©΄μ νμλκ² νλ€.
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot='sidebar-gap'
className={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)',
)}
/>
JavaScript
볡μ¬
μ μ½λλ Sidebarκ° μ°¨μ§νλ 곡κ°μ μ§μ νλ μ½λμ΄λ€. Sidebarλ Gap λ μ΄μ΄μ μ€μ 컨ν
μΈ λ₯Ό λ΄λ 컨ν
μ΄λ λ μ΄μ΄λ₯Ό λΆλ¦¬ν΄μ μ¬μ©νλ€. κ³Όκ±° μ¬μ΄λλ°λ₯Ό ꡬννμ λ sidebar wrapperμ μ§μ widthλ₯Ό μ£Όμ΄ sidebarκ° νΌμ³μ§κ³ μ ν λμ μ λλ©μ΄μ
μ μ²λ¦¬ νμλλ°, shadcn/uiλ Gap λ μ΄μ΄λ₯Ό λ³λλ‘ λμ΄ μ»¨ν
μΈ μμμ ν보 νλ€λ μ μ΄ μ κΈ°νλ€. λ€μμ μλ ₯μΌλ‘ ꡬν ν λ μ¨λ¨Ήμ΄λ³΄κ³ μΆμ μ€ν¬μ΄λ€.
transition-[width]λ width κ°μ΄ λ³κ²½λ λ κ·Έ λ³νλ₯Ό μ λλ©μ΄μ
μΌλ‘ μ²λ¦¬νλΌλ κ°μ΄λ€. group-data-[collapsible=offcanvas]:w-0μ λΆλͺ¨ μμμ data-collapsible κ°μ΄ offcanvasλ©΄ widthλ₯Ό 0μΌλ‘ μ€μ νλ€. group-data-[side=right]:rotate-180μ λΆλͺ¨ μμμ data-side κ°μ΄ rightμΌ λ rotate-180μ μ μ©νλ€. variant κ°μ΄ floatingμ΄κ±°λ insetμΌ λ data-collapsibleμ΄ iconμΌ κ²½μ° μ£Όμ΄μ§ λλΉμμ spacingμ μΆκ°νλ€. λ§μ½μ variant κ°μ΄ μ΄ μΈμ κ°μΌ κ²½μ° spacingμ μΆκ°νμ§ μλλ€.
{/* Sidebar content */}
<div
data-slot='sidebar-container'
className={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.
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,
)}
{...props}
>
...
</div>
JavaScript
볡μ¬
Sidebar 곡κ°μ ν보νκΈ° μν΄ Gap λ μ΄μ΄κ° μμλ€λ©΄ μμ μ½λλ μ€μ μ¬μ΄λλ°κ° λ λλ§ λλ 컨ν
μ΄λ λΆλΆμ΄λ€. μ¬μ΄λλ°κ° λ λλ§ λλ μμΉμ variantμ λ°λΌ μ‘°κ±΄λΆ μ€νμΌλ§μ΄ μ μ©λμ΄ μλ€. νΉμ΄ν λΆλΆμ variantμ μ‘°κ±΄μ΄ λ§μ‘±ν λ +2pxλ₯Ό νλ λΆλΆμΈλ°, μλ§ μλμ sidebar-innerμμ border λλ¬Έμ 1px * 2 ν΄μ 2pxλ§νΌμ λλΉλ₯Ό λ λΆμ¬νλ κ² κ°λ€.
<div
data-sidebar='sidebar'
data-slot='sidebar-inner'
className={cn(
'bg-sidebar flex h-full w-full flex-col',
'group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm',
)}
>
{children}
</div>
JavaScript
볡μ¬
Sidebar 컨ν
μΈ λ₯Ό λ λλ§νλ μ½λμ΄λ€. λ§μ°¬κ°μ§λ‘ variantμ μ‘°κ±΄μ΄ λ§μ‘±ν κ²½μ° μ‘°κ±΄λΆ μ€νμΌλ§μ μ μ©νλ€.
SidebarTrigger
const SidebarTrigger = ({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) => {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar='trigger'
data-slot='sidebar-trigger'
variant='ghost'
size='icon'
className={cn('size-7', className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className='sr-only'>Toggle Sidebar</span>
</Button>
);
};
TypeScript
볡μ¬
Sidebarλ₯Ό μ΄κ³ λ«μ μ μλ Trigger μ»΄ν¬λνΈμ΄λ€. shadcn/uiμ Buttonμ κ°μ Έμμ νμ©νλ€.
SidebarRail
const SidebarRail = ({
className,
...props
}: React.ComponentProps<'button'>) => {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar='rail'
data-slot='sidebar-rail'
aria-label='Toggle Sidebar'
tabIndex={-1}
onClick={toggleSidebar}
title='Toggle Sidebar'
className={cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear hover:after:bg-sidebar-border',
'group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] 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,
)}
{...props}
/>
);
};
TypeScript
볡μ¬
SliderRailμ Triggerμ²λΌ μλνλ Slider borderμ λΆμ΄μλ μ»΄ν¬λνΈμ΄λ€. λ§μ°μ€λ₯Ό μ¬λ¦¬λ©΄ μΈλ‘μ μ΄ ν
λ리μ μκΈ°λ μ»΄ν¬λνΈλ‘ μκ°νλ©΄ λλ€. λ§μ°¬κ°μ§λ‘ useSidebar ν
μ νμ©νμ¬ Sidebarμ νΌμΉ¨ μνλ₯Ό ν κΈνλ€. tabIndexλ₯Ό -1λ‘ μ£Όμ΄ ν€λ³΄λλ₯Ό μ¬μ©νμ¬ μ΄λνκ±°λ ν¬μ»€μ€λ₯Ό μ€ λ ν¬μ»€μ€κ° μ‘νμ§ μλλ‘ μ§μ νλ€. data-slotμ΄ μμΌλ©΄ aria-labelμ΄ νμ μλ€κ³ μκ°νλλ°, μ°Ύμ보λ aria-labelμ μ κ·Όμ± μ°¨μμμ νμν κ²μ΄κ³ data-* ννλ 컀μ€ν°λ§μ΄μ§ μμ±μ΄μ΄μ λ€λ₯Έ μ±κ²©μ λλ€.
μ μΌ λ³΅μ‘ν λΆλΆμ μ€νμΌλ§ λΆλΆμΈλ°, μ¬μ΄λλ°λ₯Ό νΌμ³€μ λμ μ μμ λ SidebarRailμ΄ λ³΄μ΄λ μμμ λλΉλ₯Ό κ³μ°νλλΌ μ’ μ λ¨Ήμλ€. μ€νμΌλ§μ΄ 볡μ‘νκ² λμ΄μμΌλ, νλνλ νμ
ν΄λκ°λ©΄ μ΄ν΄λ₯Ό ν μ μλ€. μΆκ°λ‘ group-data-*κΌ΄κ³Ό in-data-*κΌ΄μ μ κ·Όμκ° μλλ°, μ°Ύμ보λ λμ κΈ°λ₯μ μ μ¬νμ§λ§ groupκ³Ό λ¬λ¦¬ inμ λΆλͺ¨ μμμ group κ°μ΄ μμ΄λ groupμ²λΌ μ¬μ©ν μ μλ€. μ°Έκ³ μλ£
SidebarInset
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
return (
<main
data-slot='sidebar-inset'
className={cn(
'bg-background relative flex w-full 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,
)}
{...props}
/>
);
}
TypeScript
볡μ¬
Sidebarμ variantκ° insetμΌ κ²½μ° μ¬μ΄λλ°μ λ°°κ²½μ΄ μμ΄μ§κ³ νλ«ν ννκ° λλ€. SidebarInsetμ μ¬μ΄λλ° μ°μΈ‘μ λ©μΈ 컨ν
μΈ λ₯Ό νμν λ μ¬μ©νλ Wrapperμ μν μ μννλ€. variantκ° insetμΌ κ²½μ°μλ§ μ¬μ©ν μ μλ κ²μ μλ κ² κ°μλ°, μ€νμΌλ§μ peer-data-[variant=inset] ννκ° λλΆλΆμΈ κ²μ 보μ inset μνμμ μ£Όλ‘ μ¬μ©νλ κ²μΌλ‘ 보μΈλ€.
SidebarInput
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot='sidebar-input'
data-sidebar='input'
className={cn('bg-background h-8 w-full shadow-none', className)}
{...props}
/>
);
}
TypeScript
볡μ¬
Sidebarμμ μ¬μ©νλ Input μ»΄ν¬λνΈμ΄λ€. shadcn/uiμ Input μ»΄ν¬λνΈλ₯Ό νμ©νμ¬ Sidebarμμ μ¬μ©νλ Inputμ μ¬μ€νμΌλ§ νλ€.
SidebarHeader
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-header'
data-sidebar='header'
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
}
TypeScript
볡μ¬
Sidebar μ»΄ν¬λνΈμ μ΅μλ¨μ νμλλ SidebarHeader μ»΄ν¬λνΈμ΄λ€. μμ μμλ€μ μΈλ‘λ‘ μ λ ¬νκ³ μ¬μ΄μ κ°κ²©κ³Ό λ΄λΆ ν¨λ©μ λΆμ¬νλ€.
SidebarFooter
const SidebarFooter = ({
className,
...props
}: React.ComponentProps<'div'>) => {
return (
<div
data-slot='sidebar-footer'
data-sidebar='footer'
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
};
TypeScript
볡μ¬
Sidebar μ»΄ν¬λνΈμ μ΅νλ¨μ νμλλ SidebarFooter μ»΄ν¬λνΈμ΄λ€. μμ μμλ€μ μΈλ‘λ‘ μ λ ¬νκ³ μ¬μ΄μ κ°κ²©κ³Ό λ΄λΆ ν¨λ©μ λΆμ¬νλ€. data-* μμ± κ°μ μ μΈνκ³ λ SidebarHeaderμ ꡬν μ°¨μ΄μ μ΄ μλ€.
SidebarContent
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-content'
data-sidebar='content'
className={cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className,
)}
{...props}
/>
);
}
TypeScript
볡μ¬
Sidebar 컨ν
μΈ κ° νμλλ μμμ΄λ€. flex, min-h-0, overflow-autoλ₯Ό ν΅ν΄ μ€ν¬λ‘€λ° μ μ²΄κ° μλ 컨ν
μΈ μμλ§ μ€ν¬λ‘€μ΄ κ°λ₯νκ² λμ΄μλ€.
SidebarGroup
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-group'
data-sidebar='group'
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
{...props}
/>
);
}
TypeScript
볡μ¬
Sidebar 컨ν
μΈ λ₯Ό κ·Έλ£ΉμΌλ‘ νμνκ³ μΆμ λ μμ΄ν
μ κ°μΈλ μμμ΄λ€. relativeλ₯Ό ν΅ν΄ μμ absolute ν¬μ§μ
μμλ₯Ό λ£μ μ μλ€.
SidebarGroupAction
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<'button'> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
data-slot='sidebar-group-action'
data-sidebar='group-action'
className={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',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 md:after:hidden',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
);
}
TypeScript
볡μ¬
SidebarGroupμ μ°μΈ‘μ λΆλ λ²νΌ νΈλ¦¬κ±°μ΄λ€. absolute, top-3.5, right-3μ ν΅ν΄ μ°μλ¨μ λ λλ§ λλ κ²μ μ μ μλ€.
SidebarGroupContent
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-group-content'
data-sidebar='group-content'
className={cn('w-full text-sm', className)}
{...props}
/>
);
}
TypeScript
볡μ¬
SidebarGroup μμμ μ€μ 컨ν
μΈ λ₯Ό λ΄λ μμμ λ΄λΉνλ μ»΄ν¬λνΈμ΄λ€.
SidebarMenu & SidebarMenuItem
const SidebarMenu = ({ className, ...props }: React.ComponentProps<'ul'>) => {
return (
<ul
data-slot='sidebar-menu'
data-sidebar='menu'
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
{...props}
/>
);
};
const SidebarMenuItem = ({
className,
...props
}: React.ComponentProps<'li'>) => {
return (
<li
data-slot='sidebar-menu-item'
data-sidebar='menu-item'
className={cn('group/menu-item relative', className)}
{...props}
/>
);
};
TypeScript
볡μ¬
Sidebarμ νμ λλ λ©λ΄μ κ·Έ λ©λ΄ μμ λ€μ΄κ°λ μμ΄ν
μ λν μ»΄ν¬λνΈμ΄λ€. SidebarMenuItemμ κ²½μ° group/menu-itemκ³Ό relative μμ±μ ν΅ν΄ μμ μ»΄ν¬λνΈμμ SidebarMenuItemμ μ΄λ²€νΈλ₯Ό νμ©νμ¬ μ€νμΌλ§μ ν μ μκ³ λ΄λΆμ absolute μμλ₯Ό λ°°μΉν μ μλ€.
SidebarMenuButton
SidebarMenuButtonμ SidebarMenuItemμμ λ λλ§ λλ 컨ν
μΈ μλ κ°λ€. μ²μμ Buttonμ΄λΌκ³ ν΄μ μμ λ²νΌμΈμ€ μμλλ° MenuItemμ λ λλ§ λλ μ μ²΄κ° λ²νΌμ²λΌ λμνλ€.
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] 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',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(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',
},
},
);
JavaScript
볡μ¬
cvaλ₯Ό ν΅ν΄ μ€νμΌλ§μ ν¨μν νμ¬ μ¬μ©νλ€. μ΄λ sidebarMenuButtonVariants({variant, size}) νμμΌλ‘ λμνλ€.
const Comp = asChild ? Slot : 'button';
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot='sidebar-menu-button'
data-sidebar='menu-button'
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
TypeScript
볡μ¬
μ΄ μ»΄ν¬λνΈλ tooltip μ¬λΆλ₯Ό νλ¨νμ¬ λ¨Όμ μ‘°κ±΄λΆ λ λλ§μ νλ€. tooltipμ΄ μμ λμλ μλμ λμ¬ tooltip μ»΄ν¬λνΈλ₯Ό μμ±νμ§ μκ³ μμμ 미리 λ§λ€μ΄λμλ variantsλ₯Ό νμ©νμ¬ μ€νμΌλ§μ νλ€.
if (typeof tooltip === 'string') {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side='right'
align='center'
hidden={state !== 'collapsed' || isMobile}
{...tooltip}
/>
</Tooltip>
);
TypeScript
볡μ¬
λ§μ½ <SidebarMenuButton tooltip=βTooltipβ /> νμμΌλ‘ tooltipμ΄ stringμ΄λΌλ©΄ ꡬ쑰 ν΅μΌμ μν΄ tooltipμ childrenμ λ£κ³ κ°μ²΄ννλ€. κ·Έλ¦¬κ³ tooltipμ΄ μμΌλ©΄ shadcn/uiμ Tooltip μ»΄ν¬λνΈμ κ·Έ νμ μ»΄ν¬λνΈλ₯Ό νμ©νμ¬ μμ MenuItem(button)κ³Ό tooltipμ νμνλ€.
SidebarMenuAction
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<'button'> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
data-slot='sidebar-menu-action'
data-sidebar='menu-action'
className={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',
// Increases the hit area of the button on mobile.
'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,
)}
{...props}
/>
);
}
TypeScript
볡μ¬
SidebarMenuActionμ SidebarMenuButtonμ μ°μΈ‘ λμ λΆλ μμ μ‘μ
λ²νΌμ΄λ€. MenuButtonκ³Ό peerλ‘ κ°νκ² μ°λ λλ©° hoverμ μμ λκΈ°νμ sizeμ λ°λ₯Έ μμΉ μλ μ‘°μ λ±μ κΈ°λ₯μ μ°κ²°νλ€. menu-itemκ³Όμ μνμλ μ°λ λλ©° λ©λ΄ μμ΄ν
κ³Ό μΌμ²΄κ° μκ² λμνλ μ‘μ
λ²νΌμ΄λ€.
SidebarMenuBadge
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='sidebar-menu-badge'
data-sidebar='menu-badge'
className={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.5',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
);
}
TypeScript
볡μ¬
SidebarMenuActionκ³Ό λΉμ·νκ² λ©λ΄μ μ°μΈ‘μ λνλλ μ»΄ν¬λνΈμ΄λ€. νμ§λ§, ν¬μΈν° μ΄λ²€νΈλ₯Ό μ¬μ©ν μ μκ³ μ«μκ°μ λ¨μν λ°μ΄ν°λ₯Ό νμνλ μ©λλ‘ μ¬μ© λλ€.
SidebarMenuSkeleton
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<'div'> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot='sidebar-menu-skeleton'
data-sidebar='menu-skeleton'
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
{...props}
>
{showIcon && (
<Skeleton
className='size-4 rounded-md'
data-sidebar='menu-skeleton-icon'
/>
)}
<Skeleton
className='h-4 max-w-(--skeleton-width) flex-1'
data-sidebar='menu-skeleton-text'
style={
{
'--skeleton-width': width,
} as React.CSSProperties
}
/>
</div>
);
}
TypeScript
볡μ¬
μ¬μ΄λλ°μμ λ‘λ©κ³Ό κ°μ 컨ν
μ€νΈ λ°μ μ 보μ¬μ£Όλ μ»΄ν¬λνΈμ΄λ€. widthλ₯Ό νμ©νμ¬ μ»¨ν
μΈ μ κΈΈμ΄λ₯Ό λλ€ κΈΈμ΄λ‘ 보μ¬μ€λ€. μλνλ©΄ 컨ν
μΈ κ° λͺ¨λ κ°μ κΈΈμ΄κ° μλκΈ° λλ¬Έμ΄λ€. showIconμ trueλ‘ μ€μ νλ©΄ μ’μΈ‘μ λκ·Έλ μ€μΌλ ν€μ΄ μΆκ° λλ€.
SidebarMenuSub & SidebarMenuSubItem
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
return (
<ul
data-slot='sidebar-menu-sub'
data-sidebar='menu-sub'
className={cn(
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<'li'>) {
return (
<li
data-slot='sidebar-menu-sub-item'
data-sidebar='menu-sub-item'
className={cn('group/menu-sub-item relative', className)}
{...props}
/>
);
}
TypeScript
볡μ¬
Sidebar Submenuλ₯Ό νμνλ μ»΄ν¬λνΈμ΄λ€. SidebarMenuSubλ μλΈ λ©λ΄λ₯Ό κ°μΈλ ul νκ·Έμ΄κ³ , SidebarMenuSubItemμ SidebarMenuSub μμ λ λλ§ λλ μμ΄ν
λ€μ΄λ€.
SidebarMenuSubButton
function SidebarMenuSubButton({
asChild = false,
size = 'md',
isActive = false,
className,
...props
}: React.ComponentProps<'a'> & {
asChild?: boolean;
size?: 'sm' | 'md';
isActive?: boolean;
}) {
const Comp = asChild ? Slot : 'a';
return (
<Comp
data-slot='sidebar-menu-sub-button'
data-sidebar='menu-sub-button'
data-size={size}
data-active={isActive}
className={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,
)}
{...props}
/>
);
}
TypeScript
볡μ¬
SidebarMenuButtonκ³Ό λ§μ°¬κ°μ§λ‘ SidebarMenuSubItem μμ λ λλ§ λλ 컨ν
μΈ μ΄λ€.asChild νλΌλ―Έν°κ° trueμ΄λ©΄ childμ μ€νμΌκ³Ό λμλ§μ μ 곡ν μ μλ€.






