Motile UI

Sheet

화면 왼쪽 또는 오른쪽에서 슬라이드되는 사이드 패널 컴포넌트입니다. Compound Component 구조(Root, Trigger, Portal, Overlay, Content, Header, Title, Body, Close)로 동작하며, 설정, 필터, 네비게이션 등의 용도로 사용됩니다. 모바일 최적화를 위해 768px 이하에서는 maxWidth가 무시되고 화면 전체 너비로 표시되며, 히스토리 기반 뒤로가기 제스처를 지원합니다.

미리보기

사용법

TSX
1"use client";
2
3import { useState } from "react";
4import { Button, Sheet } from "motile-ui";
5
6export default function PreviewExample() {
7  const [open, setOpen] = useState(false);
8
9  return (
10    <div style={{ display: "flex", justifyContent: "center" }}>
11      <Sheet.Root open={open} onOpenChange={setOpen}>
12        <Sheet.Trigger asChild>
13          <Button variant="primary">Sheet 열기</Button>
14        </Sheet.Trigger>
15        <Sheet.Portal>
16          <Sheet.Overlay />
17          <Sheet.Content>
18            <Sheet.Header>
19              <Sheet.Title>Sheet 제목</Sheet.Title>
20              <Sheet.Close />
21            </Sheet.Header>
22            <Sheet.Body>
23              <p>Sheet 컴포넌트의 내용이 여기에 표시됩니다.</p>
24              <p>화면 오른쪽에서 슬라이드되는 사이드 패널입니다.</p>
25            </Sheet.Body>
26          </Sheet.Content>
27        </Sheet.Portal>
28      </Sheet.Root>
29    </div>
30  );
31}

API 레퍼런스

Sheet.Root

Sheet의 상태와 설정을 관리하는 최상위 컴포넌트

속성타입기본값설명
position"left" | "right""right"Sheet가 나타나는 위치 (left: 왼쪽, right: 오른쪽)
closeOnBackdropboolean | { escapeKey?: boolean; clickOutside?: boolean }true백드롭 클릭 또는 ESC 키로 닫기 제어 - boolean 또는 객체로 세밀한 제어 가능
maxWidthstring"600px"Sheet 최대 너비 (데스크톱 전용, 모바일은 전체 너비)
zIndexnumber1000Sheet의 z-index 값
openboolean-Sheet 열림 상태 (제어 모드)
defaultOpenbooleanfalseSheet 초기 열림 상태 (비제어 모드)
onOpenChange(open: boolean) => void-Sheet 열림 상태 변경 시 호출되는 콜백
enableHistoryClosebooleantrue히스토리 기반 뒤로가기 닫기 기능 활성화 여부. URL로 Sheet를 제어하는 경우 false로 설정하세요.

Sheet.Trigger

Sheet를 여는 트리거 요소

속성타입기본값설명
childrenReact.ReactElement-트리거로 사용할 React 요소
asChildbooleanfalse래퍼 없이 자식 요소만 렌더링

Sheet.Portal

Sheet를 특정 DOM 위치에 렌더링하는 Portal 컴포넌트

속성타입기본값설명
childrenReact.ReactNode-Portal에 렌더링할 내용
containerHTMLElementdocument.bodyPortal이 렌더링될 DOM 요소

Sheet.Overlay

Sheet 뒤에 표시되는 오버레이 배경

속성타입기본값설명
classNamestring-추가 CSS 클래스명
styleReact.CSSProperties-인라인 스타일

Sheet.Content

Sheet의 메인 콘텐츠를 담는 컴포넌트

속성타입기본값설명
childrenReact.ReactNode-Sheet 콘텐츠
classNamestring-추가 CSS 클래스명
styleReact.CSSProperties-인라인 스타일

Sheet.Header

Sheet 헤더 영역 (Title과 Close 버튼 포함)

속성타입기본값설명
childrenReact.ReactNode-Sheet 헤더 내용
classNamestring-추가 CSS 클래스명

Sheet.Title

Sheet 제목을 표시하는 컴포넌트

속성타입기본값설명
childrenReact.ReactNode-Sheet 제목 텍스트
classNamestring-추가 CSS 클래스명

Sheet.Body

Sheet 본문 내용을 담는 스크롤 가능한 영역

속성타입기본값설명
childrenReact.ReactNode-Sheet 본문 내용
classNamestring-추가 CSS 클래스명

Sheet.Close

Sheet를 닫는 버튼

속성타입기본값설명
childrenReact.ReactNodeX icon닫기 버튼 내용 (기본: X 아이콘)
asChildbooleanfalse래퍼 없이 자식 요소만 렌더링

예제

왼쪽 위치

TSX
1"use client";
2
3import { useState } from "react";
4import { Button, Sheet } from "motile-ui";
5
6export default function LeftExample() {
7  const [open, setOpen] = useState(false);
8
9  return (
10    <div style={{ display: "flex", justifyContent: "center" }}>
11      <Sheet.Root open={open} onOpenChange={setOpen} position="left">
12        <Sheet.Trigger asChild>
13          <Button variant="secondary">왼쪽 Sheet 열기</Button>
14        </Sheet.Trigger>
15        <Sheet.Portal>
16          <Sheet.Overlay />
17          <Sheet.Content>
18            <Sheet.Header>
19              <Sheet.Title>왼쪽 Sheet</Sheet.Title>
20              <Sheet.Close />
21            </Sheet.Header>
22            <Sheet.Body>
23              <p>화면 왼쪽에서 슬라이드되는 Sheet입니다.</p>
24              <p>position="left" prop을 사용했습니다.</p>
25            </Sheet.Body>
26          </Sheet.Content>
27        </Sheet.Portal>
28      </Sheet.Root>
29    </div>
30  );
31}

커스텀 닫기 버튼

TSX
1"use client";
2
3import { useState } from "react";
4import { Button, Sheet } from "motile-ui";
5
6export default function CustomCloseExample() {
7  const [open, setOpen] = useState(false);
8
9  return (
10    <div style={{ display: "flex", justifyContent: "center" }}>
11      <Sheet.Root open={open} onOpenChange={setOpen}>
12        <Sheet.Trigger asChild>
13          <Button variant="primary">커스텀 닫기 버튼</Button>
14        </Sheet.Trigger>
15        <Sheet.Portal>
16          <Sheet.Overlay />
17          <Sheet.Content>
18            <Sheet.Header>
19              <Sheet.Title>커스텀 닫기 버튼</Sheet.Title>
20              <Sheet.Close asChild>
21                <button
22                  style={{
23                    padding: "8px 16px",
24                    backgroundColor: "#ef4444",
25                    color: "white",
26                    border: "none",
27                    borderRadius: "6px",
28                    cursor: "pointer",
29                    fontSize: "14px",
30                    fontWeight: "500",
31                  }}
32                >
33                  닫기
34                </button>
35              </Sheet.Close>
36            </Sheet.Header>
37            <Sheet.Body>
38              <p>asChild prop을 사용하여 커스텀 닫기 버튼을 만들 수 있습니다.</p>
39              <p>이 예제에서는 빨간색 버튼을 사용했습니다.</p>
40            </Sheet.Body>
41          </Sheet.Content>
42        </Sheet.Portal>
43      </Sheet.Root>
44    </div>
45  );
46}

닫기 옵션 제어

TSX
1"use client";
2
3import { useState } from "react";
4import { Button, Sheet } from "motile-ui";
5
6export default function CloseOptionsExample() {
7  const [open, setOpen] = useState(false);
8
9  return (
10    <div style={{ display: "flex", justifyContent: "center" }}>
11      <Sheet.Root
12        open={open}
13        onOpenChange={setOpen}
14        closeOnBackdrop={{ escapeKey: true, clickOutside: false }}
15      >
16        <Sheet.Trigger asChild>
17          <Button variant="secondary">닫기 옵션</Button>
18        </Sheet.Trigger>
19        <Sheet.Portal>
20          <Sheet.Overlay />
21          <Sheet.Content>
22            <Sheet.Header>
23              <Sheet.Title>닫기 옵션 예제</Sheet.Title>
24              <Sheet.Close />
25            </Sheet.Header>
26            <Sheet.Body>
27              <p>ESC 키로만 닫히고 외부 클릭으로는 닫히지 않습니다.</p>
28              <p>closeOnBackdrop 옵션을 세밀하게 제어할 수 있습니다.</p>
29              <ul>
30                <li>ESC 키: 활성화</li>
31                <li>외부 클릭: 비활성화</li>
32              </ul>
33            </Sheet.Body>
34          </Sheet.Content>
35        </Sheet.Portal>
36      </Sheet.Root>
37    </div>
38  );
39}

최대 너비 설정

TSX
1"use client";
2
3import { useState } from "react";
4import { Button, Sheet } from "motile-ui";
5
6export default function MaxWidthExample() {
7  const [open, setOpen] = useState(false);
8
9  return (
10    <div style={{ display: "flex", justifyContent: "center" }}>
11      <Sheet.Root open={open} onOpenChange={setOpen} maxWidth="400px">
12        <Sheet.Trigger asChild>
13          <Button variant="ghost">좁은 Sheet</Button>
14        </Sheet.Trigger>
15        <Sheet.Portal>
16          <Sheet.Overlay />
17          <Sheet.Content>
18            <Sheet.Header>
19              <Sheet.Title>최대 너비 제한</Sheet.Title>
20              <Sheet.Close />
21            </Sheet.Header>
22            <Sheet.Body>
23              <p>maxWidth="400px"로 설정된 Sheet입니다.</p>
24              <p>데스크톱에서는 최대 400px 너비로 제한됩니다.</p>
25              <p>모바일에서는 전체 너비를 차지합니다.</p>
26            </Sheet.Body>
27          </Sheet.Content>
28        </Sheet.Portal>
29      </Sheet.Root>
30    </div>
31  );
32}

URL 기반 제어

TSX
1"use client";
2
3import { useCallback, useEffect, useState } from "react";
4import { useRouter, useSearchParams } from "next/navigation";
5import { Button, Sheet } from "motile-ui";
6
7export default function UrlControlledExample() {
8  const router = useRouter();
9  const searchParams = useSearchParams();
10  const [open, setOpen] = useState(false);
11
12  // URL의 sheet 파라미터로 열림 상태 제어
13  useEffect(() => {
14    const sheetParam = searchParams.get("sheet");
15    setOpen(sheetParam === "url-example");
16  }, [searchParams]);
17
18  // Sheet 열기: URL에 파라미터 추가
19  const handleOpen = useCallback(() => {
20    const currentUrl = new URL(window.location.href);
21    currentUrl.searchParams.set("sheet", "url-example");
22    router.push(currentUrl.pathname + currentUrl.search, { scroll: false });
23  }, [router]);
24
25  // Sheet 닫기: 뒤로가기로 이전 URL 복원
26  const handleClose = useCallback(() => {
27    router.back();
28  }, [router]);
29
30  return (
31    <Sheet.Root
32      open={open}
33      onOpenChange={(isOpen) => {
34        if (!isOpen) handleClose();
35      }}
36      enableHistoryClose={false}
37    >
38      <Button variant="secondary" onClick={handleOpen}>
39        URL 제어
40      </Button>
41      <Sheet.Portal>
42        <Sheet.Overlay />
43        <Sheet.Content>
44          <Sheet.Header>
45            <Sheet.Title>URL 기반 제어</Sheet.Title>
46            <Sheet.Close />
47          </Sheet.Header>
48          <Sheet.Body>
49            <p>이 Sheet는 URL 파라미터로 열림 상태가 제어됩니다.</p>
50            <p>
51              <code>enableHistoryClose=false</code>로 설정하면
52              히스토리 기반 닫기가 비활성화됩니다.
53            </p>
54            <ul>
55              <li>URL: ?sheet=url-example</li>
56              <li>뒤로가기: URL 변경으로 Sheet 닫힘</li>
57              <li>히스토리 중복 방지</li>
58            </ul>
59          </Sheet.Body>
60        </Sheet.Content>
61      </Sheet.Portal>
62    </Sheet.Root>
63  );
64}

Hook 레퍼런스

useSheetNavigation

Sheet 내부에서 페이지 네비게이션과 Sheet 닫기를 동시에 처리하는 hook입니다. Sheet 애니메이션이 완료된 후 페이지를 이동하여 부드러운 사용자 경험을 제공합니다. 반드시 Sheet.Root 컴포넌트 내부에서만 사용해야 합니다.

TSX
1"use client";
2
3import { Button, Sheet } from "motile-ui";
4import { useSheetNavigation } from "motile-ui/Sheet";
5import { useRouter } from "next/navigation";
6import { useState } from "react";
7
8function NavigationMenu() {
9  const router = useRouter();
10  const navigateAndClose = useSheetNavigation();
11
12  const menuItems = [
13    { label: "홈", href: "/" },
14    { label: "컴포넌트", href: "/components" },
15    { label: "Hooks", href: "/components/hooks" },
16  ];
17
18  const handleClick = (href: string) => {
19    // Sheet가 닫히는 애니메이션이 완료된 후 페이지 이동
20    navigateAndClose(() => router.push(href));
21  };
22
23  return (
24    <nav style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
25      {menuItems.map((item) => (
26        <Button
27          key={item.href}
28          variant="secondary"
29          onClick={() => handleClick(item.href)}
30        >
31          {item.label}
32        </Button>
33      ))}
34    </nav>
35  );
36}
37
38export default function Example() {
39  const [open, setOpen] = useState(false);
40
41  return (
42    <Sheet.Root open={open} onOpenChange={setOpen}>
43      <Sheet.Trigger asChild>
44        <Button variant="secondary">네비게이션 메뉴 열기</Button>
45      </Sheet.Trigger>
46      <Sheet.Portal>
47        <Sheet.Overlay />
48        <Sheet.Content>
49          <Sheet.Header>
50            <Sheet.Title>메뉴</Sheet.Title>
51            <Sheet.Close />
52          </Sheet.Header>
53          <Sheet.Body>
54            <NavigationMenu />
55          </Sheet.Body>
56        </Sheet.Content>
57      </Sheet.Portal>
58    </Sheet.Root>
59  );
60}

반환값

속성타입기본값설명
navigateAndClose(callback: () => void) => void-Sheet를 닫고 지정된 URL로 이동합니다. Sheet 닫기 애니메이션이 완료된 후 네비게이션이 실행됩니다.

Next.js Link와 함께 사용

Next.js의 Link 컴포넌트와 함께 사용할 때는 기본 동작을 막고 navigateAndClose를 호출합니다.

TSX
1"use client";
2
3import Link from "next/link";
4import { useRouter } from "next/navigation";
5import { useState } from "react";
6
7import { Button, Sheet } from "motile-ui";
8import { useSheetNavigation } from "motile-ui/Sheet";
9
10function NavLink({
11  href,
12  children,
13}: {
14  href: string;
15  children: React.ReactNode;
16}) {
17  const router = useRouter();
18  const navigateAndClose = useSheetNavigation();
19
20  return (
21    <Link
22      href={href}
23      onClick={(e) => {
24        e.preventDefault();
25        navigateAndClose(() => router.push(href));
26      }}
27      style={{
28        display: "block",
29        padding: "12px 16px",
30        color: "#333",
31        textDecoration: "none",
32        borderRadius: "8px",
33      }}
34    >
35      {children}
36    </Link>
37  );
38}
39
40export default function Example() {
41  const [open, setOpen] = useState(false);
42
43  return (
44    <Sheet.Root open={open} onOpenChange={setOpen}>
45      <Sheet.Trigger asChild>
46        <Button variant="secondary">Link 메뉴 열기</Button>
47      </Sheet.Trigger>
48      <Sheet.Portal>
49        <Sheet.Overlay />
50        <Sheet.Content>
51          <Sheet.Header>
52            <Sheet.Title>메뉴</Sheet.Title>
53            <Sheet.Close />
54          </Sheet.Header>
55          <Sheet.Body>
56            <nav
57              style={{ display: "flex", flexDirection: "column", gap: "4px" }}
58            >
59              <NavLink href="/"></NavLink>
60              <NavLink href="/components">컴포넌트</NavLink>
61              <NavLink href="/components/hooks">Hooks</NavLink>
62            </nav>
63          </Sheet.Body>
64        </Sheet.Content>
65      </Sheet.Portal>
66    </Sheet.Root>
67  );
68}

주의사항

  • 이 hook은 반드시 Sheet.Root 컴포넌트 내부에서만 사용해야 합니다.
  • Sheet 외부에서 사용하면 에러가 발생합니다.
  • 네비게이션은 Sheet 닫기 애니메이션(약 300ms)이 완료된 후 실행됩니다.
Sheet | Motile UI