# Menu URL: /lynx/components/menu Source: https://github.com/daangn/seed-design/blob/dev/docs/content/lynx/components/menu.mdx 사용자가 취할 수 있는 선택지나 액션 리스트를 제공하는 컴포넌트입니다. Lynx Engine 최소 버전: 3.9 사용 XElement: ## Preview ```tsx import "./styles"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import IconTrashcanLine from "@karrotmarket/lynx-monochrome-icon/IconTrashcanLine"; import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "@/components/ui/menu"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 열기 작업 } /> } /> } /> ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:menu - pnpm: pnpm dlx @seed-design/cli@latest add ui:menu - yarn: yarn dlx @seed-design/cli@latest add ui:menu - bun: bun x @seed-design/cli@latest add ui:menu ## Usage Menu는 Root, Trigger, Content와 Group으로 구성합니다. `MenuContent`는 native overlay, 위치 계산, 표시 수명, 긴 목록의 세로 스크롤을 함께 처리합니다. ```tsx import { ActionButton } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "@/components/ui/menu"; export function App() { return ( 열기 작업 ); } ``` - `MenuRoot`는 열림 상태와 위치를 관리합니다. - `MenuTrigger`는 닫힌 Menu를 탭으로 엽니다. - `MenuAnchor`는 위치 기준점만 제공합니다. 열림 상태는 `open`과 `onOpenChange`로 제어합니다. - `MenuContent`는 overlay와 스크롤 가능한 메뉴 목록을 렌더링합니다. 자식에 별도 `Positioner`나 `ScrollArea`를 추가하지 않습니다. - `MenuGroup`과 `MenuGroupLabel`은 관련된 항목을 묶고 제목을 표시합니다. - `MenuItem`은 `label`, 선택적 `description`, `prefixIcon`, `suffixIcon`을 한 항목으로 조립합니다. ## Props ### `MenuRoot` ### `MenuTrigger` ### `MenuAnchor` ### `MenuContent` ### `MenuGroup` ### `MenuGroupLabel` ### `MenuItem` ## Examples ### Trigger `MenuTrigger`를 탭하면 닫힌 Menu가 열립니다. `MenuItem`을 탭하거나 바깥 영역을 탭하면 닫힙니다. ## Preview ```tsx import "./styles"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import IconTrashcanLine from "@karrotmarket/lynx-monochrome-icon/IconTrashcanLine"; import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "@/components/ui/menu"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 열기 작업 } /> } /> } /> ); } ``` ### Size `size`로 Menu의 크기를 정합니다. 기본값은 `"medium"`입니다. `"responsive"`는 현재 화면 너비를 기준으로 `"small"` 또는 `"medium"`으로 바뀝니다. ```tsx import "./styles"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton, HStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, type MenuRootProps, } from "@/components/ui/menu"; function SizeMenu({ size, label }: { size: MenuRootProps["size"]; label: string }) { return ( {label} 작업 } /> } /> ); } export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( ); } ``` ### With Description `MenuItem`의 `description`에 항목의 부가 설명을 전달합니다. ```tsx import "./styles"; import IconArrowUpBracketDownLine from "@karrotmarket/lynx-monochrome-icon/IconArrowUpBracketDownLine"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 열기 } /> } /> } /> ); } ``` ### Tone `MenuItem`의 `tone="critical"`로 삭제처럼 주의가 필요한 액션을 표시합니다. ```tsx import "./styles"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import IconTrashcanLine from "@karrotmarket/lynx-monochrome-icon/IconTrashcanLine"; import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 열기 } /> } /> } /> ); } ``` ### Disabled Items `disabled` 항목은 탭해도 `bindtap`이 실행되지 않고 Menu를 닫지 않습니다. ```tsx import "./styles"; import IconArrowUpBracketDownLine from "@karrotmarket/lynx-monochrome-icon/IconArrowUpBracketDownLine"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 열기 } /> } disabled /> } /> ); } ``` ### Placement `MenuRoot`의 `placement`로 Menu를 기준 요소의 12개 방향에 배치합니다. 기본값은 `"bottom"`입니다. 화면 가장자리에서 공간이 부족하면 native runtime이 반대쪽으로 뒤집거나 경계 안으로 이동합니다. ```tsx import "./styles"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton, Box, HStack, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger, type MenuRootProps, } from "@/components/ui/menu"; function PlacementMenu({ placement }: { placement: NonNullable }) { return ( {placement} } /> } /> ); } export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( ); } ``` ### Anchor `MenuAnchor`는 Menu 위치만 정하고 탭으로 열고 닫지는 않습니다. 이 예제처럼 `open`과 `onOpenChange`로 상태를 제어할 수 있습니다. ```tsx import "./styles"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { useState } from "@lynx-js/react"; import { Box, HStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuAnchor, MenuContent, MenuGroup, MenuItem, MenuRoot } from "@/components/ui/menu"; import { Switch } from "@/components/ui/switch"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); function handleOpenChange(nextOpen: boolean, details?: { reason?: string }) { "background only"; if (!nextOpen && details?.reason === "interactOutside") return; setOpen(nextOpen); } return ( } /> } /> ); } ``` Native overlay가 열려 있는 동안에는 Anchor 바깥의 Switch를 탭할 수 없습니다. 이 예제는 Menu가 닫힌 상태에서 Switch로 열고, 항목을 선택해 닫는 흐름을 보여줍니다. overlay 바깥 탭을 무시하는 제어는 `onOpenChange`에서 `details.reason === "interactOutside"`를 걸러 구현합니다. ### Match Width `matchReferenceWidth`를 사용하면 Menu 너비가 Trigger 또는 Anchor 너비와 같아집니다. ```tsx import "./styles"; import { ActionButton, Box, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 열기 ); } ``` ### `onOpenChange` details `onOpenChange`의 두 번째 인자인 `details`에는 상태를 바꾼 이유와 native event가 들어옵니다. - 열림: `"trigger"` - 닫힘: `"trigger"`, `"interactOutside"`, `"itemClick"` - native overlay가 닫힘을 요청한 경우: `"dismiss"` ```tsx import "./styles"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { useState } from "@lynx-js/react"; import { ActionButton, HStack, VStack, useSeedClassName } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); function handleOpenChange(nextOpen: boolean, details?: { reason?: string }) { "background only"; setOpen(nextOpen); (nextOpen ? setOpenReason : setCloseReason)(details?.reason ?? null); } return ( 열기 } /> } /> 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} ); } ``` ## 웹 버전과의 차이 Lynx Menu는 native `overlay` 위에 렌더링합니다. HTML DOM, `asChild`, 키보드 포커스, typeahead, ESC 키 닫힘은 지원하지 않습니다. 따라서 `escapeKeyDown`과 `cascadeDismiss` reason도 제공하지 않습니다. 화면과 앱 수준에서 필요한 키보드 접근성은 host의 native 접근성 흐름으로 확인하세요. | 항목 | 웹 | Lynx | | ----------------- | --------------------------- | ---------------------------------------------------- | | Trigger 조합 | `asChild` 지원 | 자식을 감싸는 native Trigger를 사용 | | 위치와 목록 | DOM positioner와 scroll area | `MenuContent`가 native overlay, 위치, 스크롤을 함께 처리 | | 바깥 상호작용 | DOM pointer와 키보드 | 바깥 탭은 `interactOutside`, native dismissal은 `dismiss` | | 키보드 | ESC, focus, typeahead 지원 | 미지원 | | Anchor가 열린 동안의 제어 | 바깥 제어를 계속 조합 가능 | fullscreen overlay가 Anchor 밖의 탭을 가림 |