Search
๐Ÿ’ก

Sheet

Created
2026/01/15 15:29
Tags
2026/01/16 08:29

Context

shadcn/ui์˜ Sidebar ์ปดํฌ๋„ŒํŠธ๋Š” Mobile์ผ ํ™˜๊ฒฝ์ผ ๋•Œ Sheet ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์ด๋“œ๋ฐ”๋ฅผ ๋ Œ๋”๋งํ•œ๋‹ค. Sheet ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ด๋–ป๊ฒŒ ์“ฐ์ด๋Š”์ง€, ์™œ ํ•„์š”ํ•œ์ง€์— ๋Œ€ํ•ด์„œ ๋ถ„์„ํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.

Agenda

โ€ข
Sheet ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•ด ๋ถ„์„ํ•˜๊ณ  ์ดํ•ดํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•จ.

Contents

Extends the Dialog component to display content that complements the main content of the screen.
Plain Text
๋ณต์‚ฌ
shadcn/ui์˜ Sheet docs ์ฒซ ๋ถ€๋ถ„์— ์žˆ๋Š” ์„ค๋ช…์ด๋‹ค. Sheet ์ปดํฌ๋„ŒํŠธ๋Š” Dialog๋ฅผ ํ™•์žฅํ•˜์—ฌ ํ™”๋ฉด์— ๋ถ€๊ฐ€์ ์ธ ์š”์†Œ๋ฅผ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
<Sheet> <SheetTrigger>Open</SheetTrigger> <SheetContent> <SheetHeader> <SheetTitle>Are you absolutely sure?</SheetTitle> <SheetDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </SheetDescription> </SheetHeader> </SheetContent> </Sheet>
JavaScript
๋ณต์‚ฌ
๊ณต์‹ ๋ฌธ์„œ์— ๋‚˜์™€์žˆ๋Š” Usage์ด๋‹ค. Sheet๋ถ€ํ„ฐ SheetDescription๊นŒ์ง€์˜ ๊ณ„์ธต์œผ๋กœ ๊ตฌ์„ฑ ๋˜์–ด์žˆ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. Sidebar์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ SheetTrigger๊ฐ€ ์กด์žฌํ•˜๋ฉฐ ์ด๋Š” Sheet์˜ ํ‘œ์‹œ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์ธ ๊ฒƒ ๊ฐ™๋‹ค.
Sheet
import * as SheetPrimitive from '@radix-ui/react-dialog'; function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { return <SheetPrimitive.Root data-slot='sheet' {...props} />; }
TypeScript
๋ณต์‚ฌ
shadcn/ui์˜ sheet ํŒŒ์ผ์— ์ฒ˜์Œ์œผ๋กœ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. radix-ui์˜ react-dialog์—์„œ ๋‚˜์˜จ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์ด์ฒ˜๋Ÿผ shadcn/ui๋Š” radix-ui์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ€์ ธ์™€์„œ ์‚ฌ์šฉํ•˜๋Š” wrapper ์ฒ˜๋Ÿผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค. ์ด Sheet ์ปดํฌ๋„ŒํŠธ๋Š” Root์˜ props๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ›์•„๋‚ผ ์ˆ˜ ์žˆ๊ณ  ์‚ฌ์‹ค์ƒ Radix Dialog์˜ Root๋ฅผ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์•„๋ž˜๋Š” Sheet๊ฐ€ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ข…๋ฅ˜.
(parameter) props: { children?: React.ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?(open: boolean): void; modal?: boolean; }
JavaScript
๋ณต์‚ฌ
SheetTrigger
function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { return <SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />; }
JavaScript
๋ณต์‚ฌ
SheetTrigger๋Š” Sheet๋ฅผ ์—ด ์ˆ˜ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ radix-ui์˜ Dialog์˜ Trigger๋ฅผ ๊ฐ์‹ธ ๋ Œ๋”๋ง ํ•œ๋‹ค.
SheetClose
function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) { return <SheetPrimitive.Close data-slot='sheet-close' {...props} />; }
TypeScript
๋ณต์‚ฌ
SheetClose๋Š” Sheet๋ฅผ ๋‹ซ์„ ์ˆ˜ ์žˆ๋Š” ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. radix-ui์˜ Dialog์˜ Close๋ฅผ ๊ฐ์‹ธ์„œ ๋ Œ๋”๋งํ•œ๋‹ค.
SheetPortal
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) { return <SheetPrimitive.Portal data-slot='sheet-portal' {...props} />; }
TypeScript
๋ณต์‚ฌ
SheetPortal์€ ์‚ฌ์šฉ ์‹œ overlay์™€ ์ฝ˜ํ…์ธ ๋ฅผ body์— ๋ Œ๋”๋ง ํ•œ๋‹ค. ๋ง์ด ์‚ด์ง ์ด์ƒํ•˜๊ณ  ๋‹น์—ฐํžˆ body์— ๋ Œ๋”๋ง ํ•˜๋Š”๋ฐ, ์ด Portal ๊ธฐ๋Šฅ์€ z-index๋‚˜ overflow ๊ฐ™์€ ๋ Œ๋”๋ง ๊ณ„์ธต ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…ผ๋ฆฌ์ ์œผ๋กœ๋Š” ๋ฐฐ์น˜ํ•œ ๊ณณ์— ์†Œ์† ์‹œํ‚ค์ง€๋งŒ ์‹ค์ œ ๋ Œ๋”๋ง์€ body ํƒœ๊ทธ์™€ ๊ฐ€๊นŒ์šด ๊ณณ์— ๋ Œ๋”๋งํ•œ๋‹ค.
SheetOverlay
function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) { return ( <SheetPrimitive.Overlay data-slot='sheet-overlay' className={cn( 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50', className, )} {...props} /> ); }
TypeScript
๋ณต์‚ฌ
SheetOverlay ์ปดํฌ๋„ŒํŠธ๋Š” Sheet๊ฐ€ ์—ด๋ ธ์„ ๋•Œ ๋’ค ๋ฐฐ๊ฒฝ์„ ์–ด๋‘ก๊ฒŒ ํ•ด์ฃผ๋Š” ๋ฐ˜ํˆฌ๋ช… ๋ ˆ์ด์–ด์ด๋‹ค. Backdrop ํšจ๊ณผ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋ฉฐ ์ ‘๊ทผ์„ฑ, ํฌ์ปค์Šค, ์Šคํฌ๋กค๋ฝ๊ณผ ๊ฐ™์ด ๋ชจ๋‹ฌ ๋ฐฐ๊ฒฝ์„ ๊ตฌ์„ฑํ•˜๋Š” ์š”์†Œ์ด๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ radix-ui Dialog์˜ Overlay๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
SheetContent
function SheetContent({ className, children, side = 'right', ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & { side?: 'top' | 'right' | 'bottom' | 'left'; }) { return ( <SheetPortal> <SheetOverlay /> <SheetPrimitive.Content data-slot='sheet-content' className={cn( 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500', side === 'right' && 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm', side === 'left' && 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm', side === 'top' && 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b', side === 'bottom' && 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t', className, )} {...props} > {children} <SheetPrimitive.Close className='ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none'> <XIcon className='size-4' /> <span className='sr-only'>Close</span> </SheetPrimitive.Close> </SheetPrimitive.Content> </SheetPortal> ); }
TypeScript
๋ณต์‚ฌ
Sheet์˜ ๋‚ด์šฉ์„ ๋ณด์—ฌ์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. side ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ™œ์šฉํ•ด Sheet๊ฐ€ ํŽผ์ณ์ง€๊ณ  ์ ‘ํžˆ๋Š” ๋ฐฉํ–ฅ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ง€๊ธˆ๊นŒ์ง€ Overlay์˜ children์— Content๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์—ˆ๋Š”๋ฐ, shadcn/ui์—์„œ๋Š” Overlay์™€ Content๋ฅผ sibling depth์— ๋‘์–ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•˜์˜€๋‹ค. ์•„๋งˆ ํด๋ฆญ๊ณผ ๊ฐ™์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์™€ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋Šฅ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ตฌ์กฐ๊ฐ€ ์•„๋‹๊นŒ ์ƒ๊ฐํ•œ๋‹ค.
const SheetHeader = ({ className, ...props }: React.ComponentProps<'div'>) => { return ( <div data-slot='sheet-header' className={cn('flex flex-col gap-1.5 p-4', className)} {...props} /> ); }; const SheetFooter = ({ className, ...props }: React.ComponentProps<'div'>) => { return ( <div data-slot='sheet-footer' className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
Sheet์˜ Header์™€ Footer ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. radix-ui์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๊ตฌ์„ฑ๋˜์–ด์žˆ๋‹ค.
Sidebar์—์„œ ์‚ฌ์šฉ๋œ ์˜ˆ์‹œ๋ฅผ ๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด sr-only๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ™”๋ฉด์—๋Š” ์•ˆ๋ณด์ด๊ฒŒ ํ•˜๊ณ  ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž์—๊ฒŒ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค.
<SheetHeader className='sr-only'> <SheetTitle>Sidebar</SheetTitle> <SheetDescription>Displays the mobile sidebar.</SheetDescription> </SheetHeader>
TypeScript
๋ณต์‚ฌ
const SheetTitle = ({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) => { return ( <SheetPrimitive.Title data-slot='sheet-title' className={cn('text-foreground font-semibold', className)} {...props} /> ); }; const SheetDescription = ({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) => { return ( <SheetPrimitive.Description data-slot='sheet-description' className={cn('text-muted-foreground text-sm', className)} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
Sheet์˜ Title๊ณผ Description์ด๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ h2 ํƒœ๊ทธ์™€ p ํƒœ๊ทธ๋ฅผ ํ™œ์šฉํ•ด์„œ ์ œ๋ชฉ๊ณผ ๊ทธ ์ •๋ณด๋ฅผ ๋งˆํฌ์—… ํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ์ ‘๊ทผ์„ฑ ์ฐจ์›์—์„œ Title๊ณผ Description์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์— ๋ณด๋ฉด Title๊ณผ Description์ด announced ๋œ๋‹ค๊ณ  ๋‚˜์™€์žˆ๋Š”๋ฐ ์•„๋งˆ ์ด๋Ÿฐ ๋งฅ๋ฝ์—์„œ์˜ ์„ค๋ช…์ด๋ผ๊ณ  ๋ฐ›์•„๋“ค์—ฌ์ง„๋‹ค.