Search
moon
sun
πŸ’‘

Sidebar

Created
2026/01/14 10:25
Tags
2026/01/18 12:31

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에 μŠ€νƒ€μΌκ³Ό λ™μž‘λ§Œμ„ μ œκ³΅ν•  수 μžˆλ‹€.