Menu
사용자가 취할 수 있는 선택지나 액션 리스트를 제공하는 컴포넌트입니다.
import {
IconPencilLine,
IconPlusLine,
IconTrashcanLine,
} from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuContent,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuRoot,
MenuTrigger,
} from "seed-design/ui/menu";
export default function MenuPreview() {
return (
<MenuRoot>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuGroupLabel>작업</MenuGroupLabel>
<MenuItem label="라이브러리에 추가" prefixIcon={<IconPlusLine />} />
<MenuItem
label="수정"
description="현재 항목을 수정합니다"
prefixIcon={<IconPencilLine />}
/>
</MenuGroup>
<MenuGroup>
<MenuItem
label="삭제"
description="이 작업은 되돌릴 수 없습니다"
tone="critical"
prefixIcon={<IconTrashcanLine />}
/>
</MenuGroup>
</MenuContent>
</MenuRoot>
);
}Installation
npx @seed-design/cli@latest add ui:menupnpm dlx @seed-design/cli@latest add ui:menuyarn dlx @seed-design/cli@latest add ui:menubun x @seed-design/cli@latest add ui:menu의존성 설치
npm install @seed-design/reactyarn add @seed-design/reactpnpm add @seed-design/reactbun add @seed-design/react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:menu
* @requires @seed-design/react@^2.0.0
* @requires @seed-design/css@^2.0.0
**/
"use client";
import { PrefixIcon, SuffixIcon, Menu as SeedMenu } from "@seed-design/react";
import * as React from "react";
export interface MenuRootProps extends SeedMenu.RootProps {}
/**
* @see https://seed-design.io/react/components/menu
*/
export const MenuRoot = SeedMenu.Root;
export interface MenuAnchorProps extends SeedMenu.AnchorProps {}
/**
* @see https://seed-design.io/react/components/menu
*/
export const MenuAnchor = SeedMenu.Anchor;
export interface MenuTriggerProps extends SeedMenu.TriggerProps {}
/**
* @see https://seed-design.io/react/components/menu
*/
export const MenuTrigger = SeedMenu.Trigger;
export interface MenuContentProps extends SeedMenu.ContentProps {
positionerContainer?: SeedMenu.PositionerProps["container"];
}
/**
* @see https://seed-design.io/react/components/menu
*/
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
({ children, positionerContainer, ...props }, ref) => {
return (
<SeedMenu.Positioner container={positionerContainer}>
<SeedMenu.Content ref={ref} {...props}>
<SeedMenu.ScrollArea>{children}</SeedMenu.ScrollArea>
</SeedMenu.Content>
</SeedMenu.Positioner>
);
},
);
MenuContent.displayName = "MenuContent";
export interface MenuGroupProps extends SeedMenu.GroupProps {}
/**
* @see https://seed-design.io/react/components/menu
*/
export const MenuGroup = SeedMenu.Group;
export interface MenuGroupLabelProps extends SeedMenu.GroupLabelProps {}
/**
* @see https://seed-design.io/react/components/menu
*/
export const MenuGroupLabel = SeedMenu.GroupLabel;
export interface MenuItemProps extends Omit<SeedMenu.ItemProps, "children"> {
prefixIcon?: React.ReactNode;
label: React.ReactNode;
description?: React.ReactNode;
suffixIcon?: React.ReactNode;
}
/**
* @see https://seed-design.io/react/components/menu
*/
export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
({ prefixIcon, label, description, suffixIcon, ...props }, ref) => {
return (
<SeedMenu.Item ref={ref} {...props}>
{prefixIcon && <PrefixIcon svg={prefixIcon} />}
<SeedMenu.ItemBody>
<SeedMenu.ItemLabel>{label}</SeedMenu.ItemLabel>
{description && <SeedMenu.ItemDescription>{description}</SeedMenu.ItemDescription>}
</SeedMenu.ItemBody>
{suffixIcon && <SuffixIcon svg={suffixIcon} />}
</SeedMenu.Item>
);
},
);
MenuItem.displayName = "MenuItem";
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
Usage
Menu는 아래와 같은 구조로 구성됩니다.
import {
MenuRoot,
MenuTrigger,
MenuContent,
MenuGroup,
MenuGroupLabel,
MenuItem,
} from "seed-design/ui/menu";<MenuRoot>
<MenuTrigger>...</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuGroupLabel>그룹 제목</MenuGroupLabel>
<MenuItem label="항목 1" />
<MenuItem label="항목 2" />
</MenuGroup>
<MenuGroup>
<MenuItem label="항목 3" />
</MenuGroup>
</MenuContent>
</MenuRoot>MenuRoot: 메뉴의 상태(열림/닫힘)와 위치를 관리합니다.MenuTrigger: 클릭 시 메뉴를 열고 닫는 트리거입니다.MenuAnchor: 메뉴의 위치 기준점만 제공하며, 열고 닫는 동작은 포함하지 않습니다. 트리거 동작을 직접 제어해야 하는 경우에MenuTrigger대신 사용합니다.MenuContent: 메뉴 항목들을 감싸는 플로팅 컨테이너입니다.MenuGroup: 관련된 항목들을 그룹으로 묶습니다. 모든MenuItem은MenuGroup안에 있어야 합니다.MenuGroupLabel: 그룹의 제목을 표시합니다.MenuItem: 개별 메뉴 항목입니다.
Props
MenuRoot
Prop
Type
children?React.ReactNodedisabled?boolean | undefinedopen?boolean | undefineddefaultOpen?boolean | undefinedonOpenChange?((open: boolean, details?: MenuChangeDetails) => void) | undefinedMenuTrigger
Prop
Type
MenuAnchor
Prop
Type
MenuContent
Prop
Type
positionerContainer?React.RefObject<HTMLElement | null> | undefinedMenuGroup
Prop
Type
MenuGroupLabel
Prop
Type
MenuItem
Prop
Type
prefixIcon?React.ReactNodelabelReact.ReactNodedescription?React.ReactNodesuffixIcon?React.ReactNodedisabled?boolean | undefinedExamples
Trigger
<MenuTrigger>는 aria-haspopup="menu" 속성을 설정하고, Menu의 open 상태에 따라 aria-expanded 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다.
import {
IconPencilLine,
IconPlusLine,
IconTrashcanLine,
} from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuContent,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuRoot,
MenuTrigger,
} from "seed-design/ui/menu";
export default function MenuPreview() {
return (
<MenuRoot>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuGroupLabel>작업</MenuGroupLabel>
<MenuItem label="라이브러리에 추가" prefixIcon={<IconPlusLine />} />
<MenuItem
label="수정"
description="현재 항목을 수정합니다"
prefixIcon={<IconPencilLine />}
/>
</MenuGroup>
<MenuGroup>
<MenuItem
label="삭제"
description="이 작업은 되돌릴 수 없습니다"
tone="critical"
prefixIcon={<IconTrashcanLine />}
/>
</MenuGroup>
</MenuContent>
</MenuRoot>
);
}Size
size로 MenuRoot의 크기를 정합니다. (default: medium)
responsive는 화면 너비에 따라 size가 자동으로 전환되는 값입니다. 여러 화면 너비를 함께 지원하는 제품에서 size=responsive를 사용하여 대응합니다.
import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon";
import { HStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuContent,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuRoot,
MenuTrigger,
} from "seed-design/ui/menu";
export default function MenuSize() {
return (
<HStack gap="x4">
<MenuRoot size="medium">
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">Medium</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuGroupLabel>작업</MenuGroupLabel>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem label="수정" prefixIcon={<IconPencilLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
<MenuRoot size="small">
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">Small</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuGroupLabel>작업</MenuGroupLabel>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem label="수정" prefixIcon={<IconPencilLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
<MenuRoot size="responsive">
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">Responsive</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuGroupLabel>작업</MenuGroupLabel>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem label="수정" prefixIcon={<IconPencilLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
</HStack>
);
}With Description
MenuItem의 description prop을 사용하여 항목에 부가 설명을 추가합니다.
import {
IconArrowUpBracketDownLine,
IconPencilLine,
IconPlusLine,
} from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu";
export default function MenuWithDescription() {
return (
<MenuRoot>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem
label="라이브러리에 추가"
description="내 라이브러리에 항목을 추가합니다"
prefixIcon={<IconPlusLine />}
/>
<MenuItem
label="수정"
description="현재 항목을 수정합니다"
prefixIcon={<IconPencilLine />}
/>
<MenuItem
label="공유"
description="다른 사람에게 공유합니다"
prefixIcon={<IconArrowUpBracketDownLine />}
/>
</MenuGroup>
</MenuContent>
</MenuRoot>
);
}Tone
MenuItem의 tone prop으로 항목의 톤을 설정합니다.
import {
IconPencilLine,
IconPlusLine,
IconTrashcanLine,
} from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu";
export default function MenuTone() {
return (
<MenuRoot>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem label="수정" prefixIcon={<IconPencilLine />} />
</MenuGroup>
<MenuGroup>
<MenuItem label="삭제" tone="critical" prefixIcon={<IconTrashcanLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
);
}Disabled Items
MenuItem의 disabled prop을 사용하여 특정 항목을 비활성화합니다.
import {
IconArrowUpBracketDownLine,
IconPencilLine,
IconPlusLine,
} from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu";
export default function MenuDisabled() {
return (
<MenuRoot>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem
label="수정"
description="현재 항목을 수정합니다"
prefixIcon={<IconPencilLine />}
disabled
/>
<MenuItem label="공유" prefixIcon={<IconArrowUpBracketDownLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
);
}Placement
MenuRoot의 placement prop으로 메뉴의 위치를 설정합니다. 기본값은 "bottom"입니다.
import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon";
import { Box } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuContent,
MenuGroup,
MenuItem,
MenuRoot,
MenuTrigger,
type MenuRootProps,
} from "seed-design/ui/menu";
function PlacementMenu({ placement }: { placement: NonNullable<MenuRootProps["placement"]> }) {
return (
<MenuRoot placement={placement}>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">{placement}</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem label="수정" prefixIcon={<IconPencilLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
);
}
export default function MenuPlacement() {
return (
<div
style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "80px", padding: "80px" }}
>
<PlacementMenu placement="top-end" />
<PlacementMenu placement="top" />
<PlacementMenu placement="top-start" />
<PlacementMenu placement="left-end" />
<Box />
<PlacementMenu placement="right-end" />
<PlacementMenu placement="left" />
<Box />
<PlacementMenu placement="right" />
<PlacementMenu placement="left-start" />
<Box />
<PlacementMenu placement="right-start" />
<PlacementMenu placement="bottom-end" />
<PlacementMenu placement="bottom" />
<PlacementMenu placement="bottom-start" />
</div>
);
}Anchor
MenuAnchor의 children은 Menu가 위치를 잡는 데에만 사용되며, MenuTrigger와 달리 클릭으로 열고 닫는 동작이 포함되지 않습니다.
open prop을 controlled하게 사용하여 열리고 닫힌 상태를 직접 관리해야 합니다.
아래 예시의 경우 onOpenChange 콜백에 조건을 추가하여 바깥 영역을 클릭한 경우에도 메뉴가 닫히지 않도록 구현되어 있습니다.
import { Avatar } from "seed-design/ui/avatar";
import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder";
import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon";
import { useState } from "react";
import { Switch } from "seed-design/ui/switch";
import { MenuAnchor, MenuContent, MenuGroup, MenuItem, MenuRoot } from "seed-design/ui/menu";
import { HStack } from "@seed-design/react";
export default function MenuAnchorExample() {
const [open, setOpen] = useState(false);
return (
<HStack align="center" justify="space-between" width="full">
<Switch tone="neutral" label="메뉴" checked={open} onCheckedChange={setOpen} />
<MenuRoot
open={open}
onOpenChange={(nextOpen, details) => {
if (!nextOpen && details?.reason === "interactOutside") return;
setOpen(nextOpen);
}}
>
<MenuAnchor asChild>
<Avatar
size="80"
src="https://avatars.githubusercontent.com/u/54893898?v=4"
fallback={<IdentityPlaceholder />}
/>
</MenuAnchor>
<MenuContent>
<MenuGroup>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem label="수정" prefixIcon={<IconPencilLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
</HStack>
);
}Match Width
MenuRoot의 matchReferenceWidth prop을 사용하면 메뉴의 너비가 reference 요소(Trigger 또는 Anchor)의 너비에 맞춰집니다.
import { ActionButton } from "seed-design/ui/action-button";
import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu";
export default function MenuMatchReferenceWidth() {
return (
<MenuRoot matchReferenceWidth>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid" style={{ width: 400, maxWidth: "100%" }}>
열기
</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem label="추가" />
<MenuItem label="수정" />
<MenuItem label="공유" />
</MenuGroup>
</MenuContent>
</MenuRoot>
);
}onOpenChange Details
onOpenChange 두 번째 인자로 details가 제공됩니다.
reason
열릴 때 (open: true)
"trigger":MenuTrigger로 열림
닫힐 때 (open: false)
"trigger":MenuTrigger로 닫힘"escapeKeyDown": ESC 키 사용"interactOutside": 외부 영역 클릭"itemClick": 메뉴 항목 클릭"cascadeDismiss": 상위 레이어 닫힘으로 인한 연쇄 닫힘
import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon";
import { HStack, Text, VStack } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu";
export default function MenuOpenChangeReason() {
const [open, setOpen] = useState(false);
const [openReason, setOpenReason] = useState<string | null>(null);
const [closeReason, setCloseReason] = useState<string | null>(null);
return (
<VStack gap="x4" align="center">
<MenuRoot
open={open}
onOpenChange={(open, details) => {
setOpen(open);
(open ? setOpenReason : setCloseReason)(details?.reason ?? null);
}}
>
<MenuTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</MenuTrigger>
<MenuContent>
<MenuGroup>
<MenuItem label="추가" prefixIcon={<IconPlusLine />} />
<MenuItem label="수정" prefixIcon={<IconPencilLine />} />
</MenuGroup>
</MenuContent>
</MenuRoot>
<HStack gap="x4">
<Text fontSize="t3" color="fg.neutralMuted">
마지막 열림 이유: {openReason ?? "-"}
</Text>
<Text fontSize="t3" color="fg.neutralMuted">
마지막 닫힘 이유: {closeReason ?? "-"}
</Text>
</HStack>
</VStack>
);
}Last updated on