List
List 컴포넌트는 정보를 구조화된 목록 형태로 표시하는 데 사용됩니다.
import { List, ListDivider, ListItem } from "seed-design/ui/list";
import { ListHeader } from "seed-design/ui/list-header";
import {
IconILowercaseSerifCircleLine,
IconPersonCircleLine,
} from "@karrotmarket/react-monochrome-icon";
import { Icon, VStack } from "@seed-design/react";
export default function ListPreview() {
return (
<VStack width="360px">
<ListHeader as="h2">리스트 헤더</ListHeader>
<List width="full">
<ListItem title="기본 리스트 아이템" />
<ListDivider />
<ListItem
prefix={<Icon svg={<IconPersonCircleLine />} />}
title="아이콘이 있는 리스트 아이템"
detail="부가 정보가 포함된 설명"
suffix={<Icon svg={<IconILowercaseSerifCircleLine />} />}
/>
</List>
</VStack>
);
}Installation
npx @seed-design/cli@latest add ui:listAnatomy
import { ListHeader } from "seed-design/ui/list-header";
import { List, ListItem } from "seed-design/ui/list";
<VStack>
<ListHeader as="h2">리스트 헤더</ListHeader>
<List>
<ListItem title="리스트 아이템 1" />
<ListDivider />
<ListItem title="리스트 아이템 2" />
</List>
</VStack>Props
ListHeader
Prop
Type
List
VStackProps와 동일합니다.
List Items
Examples
Using ListHeader
ListHeader는 List 밖에 위치합니다.
import { List, ListButtonItem } from "seed-design/ui/list";
import { ListHeader } from "seed-design/ui/list-header";
import {
IconChevronRightLine,
IconLockLine,
IconPersonCircleLine,
} from "@karrotmarket/react-monochrome-icon";
import { Divider, Icon, VStack } from "@seed-design/react";
export default function () {
return (
<VStack gap="x6" py="x6" width="360px">
<VStack>
<ListHeader as="h2" variant="mediumWeak">
variant="mediumWeak"
</ListHeader>
<List>
<ListButtonItem
title="내 계정"
detail="이메일과 연락처, 본인 인증 관리"
prefix={<Icon svg={<IconPersonCircleLine />} />}
suffix={<Icon svg={<IconChevronRightLine />} size="18px" />}
/>
<ListButtonItem
title="보안 · 인증 관리"
detail="비밀번호, 생체 인증 사용을 관리해요"
prefix={<Icon svg={<IconLockLine />} />}
suffix={<Icon svg={<IconChevronRightLine />} size="x4_5" />}
/>
</List>
</VStack>
<Divider />
<VStack>
<ListHeader as="h2" variant="boldSolid">
variant="boldSolid"
</ListHeader>
<List>
<ListButtonItem
title="내 계정"
detail="이메일과 연락처, 본인 인증 관리"
prefix={<Icon svg={<IconPersonCircleLine />} />}
suffix={<Icon svg={<IconChevronRightLine />} size="18px" />}
/>
<ListButtonItem
title="보안 · 인증 관리"
detail="비밀번호, 생체 인증 사용을 관리해요"
prefix={<Icon svg={<IconLockLine />} />}
suffix={<Icon svg={<IconChevronRightLine />} size="x4_5" />}
/>
</List>
</VStack>
</VStack>
);
}Affixes (Prefix/Suffix)
import {
IconArrowUpBracketDownLine,
IconILowercaseSerifCircleLine,
} from "@karrotmarket/react-monochrome-icon";
import { Icon } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import { Avatar } from "seed-design/ui/avatar";
import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder";
import { List, ListDivider, ListItem } from "seed-design/ui/list";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ListAffixes() {
const [isToggleButtonPressed, setIsToggleButtonPressed] = useState(false);
return (
<List width="360px">
<ListItem
prefix={
<Avatar
size="48"
src="https://avatars.githubusercontent.com/u/54893898?v=4"
fallback={<IdentityPlaceholder />}
/>
}
title="Prefix에 Avatar 넣기"
detail="Amet elit ullamco magna."
/>
<ListDivider />
<ListItem
prefix={<Avatar size="48" fallback={<IdentityPlaceholder />} />}
title="Prefix에 Avatar 넣고 상단으로 정렬하기. 일반적으로 `title`이 길어질 때 사용합니다. Veniam elit velit esse ea incididunt sunt sit aute."
detail="Et proident sit ullamco ut voluptate."
alignItems="flex-start"
/>
<ListDivider />
<ListItem
title="Prefix에 아이콘 넣기"
detail="Deserunt nulla elit est."
prefix={<Icon svg={<IconILowercaseSerifCircleLine />} />}
/>
<ListDivider />
<ListItem
title="Suffix에 Action Button 넣기"
detail="Veniam non est non ut consequat."
suffix={
<ActionButton variant="neutralWeak" size="xsmall">
액션 버튼
</ActionButton>
}
/>
<ListDivider />
<ListItem
title="Suffix에 Action Button (Ghost) 넣기"
detail="Deserunt nulla elit est."
suffix={
<ActionButton size="small" variant="ghost" layout="iconOnly" aria-label="공유">
<Icon svg={<IconArrowUpBracketDownLine />} />
</ActionButton>
}
/>
<ListDivider />
<ListItem
title="Suffix에 Toggle Button 넣기"
detail="Sit eu incididunt aute ea elit ex."
suffix={
<ToggleButton
size="xsmall"
pressed={isToggleButtonPressed}
onPressedChange={setIsToggleButtonPressed}
>
{isToggleButtonPressed ? "선택됨" : "토글 버튼"}
</ToggleButton>
}
/>
</List>
);
}Clickable List Items
ListButtonItem 또는 ListLinkItem를 사용해서 리스트 항목 전체를 클릭 가능하도록 만들 수 있습니다.
import {
IconArrowUpRightLine,
IconCheckmarkFill,
IconChevronRightLine,
IconPlusFill,
IconSquare2StackedFill,
} from "@karrotmarket/react-monochrome-icon";
import { PrefixIcon, Icon } from "@seed-design/react";
import { useCallback, useState } from "react";
import { List, ListDivider, ListItem, ListButtonItem, ListLinkItem } from "seed-design/ui/list";
import { ActionButton } from "seed-design/ui/action-button";
import { ToggleButton } from "seed-design/ui/toggle-button";
const href = "https://www.daangn.com";
export default function ListClickable() {
const [isSubscribed, setIsSubscribed] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const onCopyClick = useCallback(() => {
navigator.clipboard.writeText(href);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
}, []);
return (
<List width="full">
<ListItem
title="ListItem은 클릭할 수 없어요. 눌러보세요."
detail="우측의 Action Button만 클릭할 수 있어요"
suffix={
<ActionButton variant="neutralWeak" size="xsmall" onClick={() => alert("편집 클릭됨")}>
편집
</ActionButton>
}
/>
<ListDivider />
<ListButtonItem
title="ListButtonItem은 클릭할 수 있어요. 눌러보세요."
detail="리스트 항목 전체와 우측의 Toggle Button 각각을 클릭할 수 있어요"
onClick={() => alert("리스트 아이템 클릭됨")}
suffix={
<>
<ToggleButton size="xsmall" pressed={isSubscribed} onPressedChange={setIsSubscribed}>
<PrefixIcon svg={isSubscribed ? <IconCheckmarkFill /> : <IconPlusFill />} />
{isSubscribed ? "모아보는 중" : "모아보기"}
</ToggleButton>
<Icon svg={<IconChevronRightLine />} />
</>
}
/>
<ListDivider />
<ListLinkItem
title="ListLinkItem도 클릭할 수 있어요. 눌러보세요."
detail="리스트 항목 전체와 우측의 Action Button 각각을 클릭할 수 있어요"
suffix={
<>
<ActionButton variant="neutralWeak" size="xsmall" onClick={onCopyClick}>
<PrefixIcon svg={isCopied ? <IconCheckmarkFill /> : <IconSquare2StackedFill />} />
{isCopied ? "복사됨" : "URL 복사"}
</ActionButton>
<Icon svg={<IconArrowUpRightLine />} />
</>
}
href={href}
target="_blank"
rel="noreferrer"
/>
</List>
);
}inputs in List Items
ListSwitchItem, ListCheckItem, ListRadioItem을 사용해서 리스트 항목에 input 요소를 포함할 수 있습니다. 이때, SwitchMark, Checkmark 또는 RadioMark와 같은 컨트롤 요소를 prefix나 suffix 영역에 넣어 사용합니다.
import { IconTrashcanLine } from "@karrotmarket/react-monochrome-icon";
import { IconSparkle2 } from "@karrotmarket/react-multicolor-icon";
import { Icon } from "@seed-design/react";
import { List, ListDivider, ListSwitchItem } from "seed-design/ui/list";
import { SwitchMark } from "seed-design/ui/switch";
export default function ListSwitch() {
return (
<List width="360px">
<ListSwitchItem
title="삭제하기 전에 확인"
prefix={<Icon svg={<IconTrashcanLine />} />}
suffix={<SwitchMark tone="neutral" />}
/>
<ListDivider />
<ListSwitchItem
title="메시지 요약"
detail="핵심 내용만 빠르게 확인해보세요."
prefix={<Icon svg={<IconSparkle2 />} />}
suffix={<SwitchMark tone="neutral" />}
defaultChecked
/>
</List>
);
}import { Badge, HStack } from "@seed-design/react";
import { List, ListDivider, ListCheckItem } from "seed-design/ui/list";
import { Checkmark } from "seed-design/ui/checkbox";
export default function ListCheckbox() {
return (
<List as="fieldset" width="360px">
<ListCheckItem
title={
<HStack gap="x1_5">
<span>알림 수신 동의</span>
<Badge variant="weak">권장</Badge>
</HStack>
}
detail="푸시 알림을 받으시겠습니까?"
suffix={<Checkmark tone="neutral" size="large" />}
defaultChecked
/>
<ListDivider as="div" />
<ListCheckItem
prefix={<Checkmark tone="neutral" size="large" />}
title="마케팅 정보 수신 동의"
detail="마케팅 정보를 받으시겠습니까?"
defaultChecked
/>
<ListDivider as="div" />
<ListCheckItem
prefix={<Checkmark tone="neutral" size="large" variant="ghost" />}
title="Ghost Variant"
/>
</List>
);
}import { RadioGroup } from "@seed-design/react";
import { List, ListDivider, ListRadioItem } from "seed-design/ui/list";
import { RadioMark } from "seed-design/ui/radio-group";
export default function ListRadio() {
return (
<List width="360px" asChild>
<RadioGroup.Root defaultValue="option1" aria-label="옵션 선택">
<ListRadioItem
value="option1"
title="옵션 1"
detail="첫 번째 선택지"
suffix={<RadioMark tone="neutral" size="large" />}
/>
<ListDivider as="div" />
<ListRadioItem
prefix={<RadioMark tone="neutral" size="large" />}
value="option2"
title="옵션 2"
detail="두 번째 선택지"
/>
<ListDivider as="div" />
<ListRadioItem
prefix={<RadioMark tone="neutral" size="large" />}
value="option3"
title="옵션 3"
detail="세 번째 선택지"
/>
</RadioGroup.Root>
</List>
);
}Accessibility
List는 기본적으로 <ul>입니다. ListCheckItem와 ListRadioItem를 사용하는 경우 List에 적절한 role을 부여해야 합니다.
<List as="fieldset">
<ListCheckItem
suffix={<Checkmark size="large" />}
title="알림 수신 동의"
detail="푸시 알림을 받으시겠습니까?"
/>
<ListCheckItem
suffix={<Checkmark size="large" />}
title="마케팅 정보 수신 동의"
detail="마케팅 정보를 받으시겠습니까?"
/>
</List><List asChild>
<RadioGroup.Root defaultValue="짜장" aria-label="점심 메뉴"> {/* <div role="radiogroup"> */}
<ListRadioItem
suffix={<RadioMark size="large" />}
value="짜장"
title="짜장"
/>
<ListRadioItem
suffix={<RadioMark size="large" />}
value="짬뽕"
title="짬뽕"
/>
</RadioGroup.Root>
</List>Disabled
import {
IconChevronRightLine,
IconPersonCircleLine,
IconSlashCircleLine,
} from "@karrotmarket/react-monochrome-icon";
import { Divider, Icon, RadioGroup, VStack } from "@seed-design/react";
import { List, ListButtonItem, ListCheckItem, ListRadioItem } from "seed-design/ui/list";
import { Checkmark } from "seed-design/ui/checkbox";
import { RadioMark } from "seed-design/ui/radio-group";
export default function ListDisabled() {
return (
<VStack width="360px">
<List>
<ListButtonItem
prefix={<Icon svg={<IconPersonCircleLine />} />}
title="활성화된 ListButtonItem"
suffix={<Icon svg={<IconChevronRightLine />} />}
/>
</List>
<List as="fieldset">
<ListCheckItem
prefix={<Icon svg={<IconPersonCircleLine />} />}
title="활성화된 ListCheckItem"
suffix={<Checkmark tone="neutral" size="large" />}
/>
</List>
<List asChild>
<RadioGroup.Root defaultValue="foo" aria-label="옵션 선택">
<ListRadioItem
prefix={<Icon svg={<IconPersonCircleLine />} />}
title="활성화된 ListRadioItem"
suffix={<RadioMark tone="neutral" size="large" />}
value="foo"
/>
</RadioGroup.Root>
</List>
<Divider />
<List>
<ListButtonItem
disabled
prefix={<Icon svg={<IconSlashCircleLine />} />}
title="비활성화된 ListButtonItem"
suffix={<Icon svg={<IconChevronRightLine />} />}
/>
</List>
<List as="fieldset">
<ListCheckItem
disabled
prefix={<Icon svg={<IconSlashCircleLine />} />}
title="비활성화된 ListCheckItem"
suffix={<Checkmark tone="neutral" size="large" />}
/>
</List>
<List asChild>
<RadioGroup.Root defaultValue="foo" aria-label="옵션 선택">
<ListRadioItem
disabled
prefix={<Icon svg={<IconSlashCircleLine />} />}
title="비활성화된 ListRadioItem"
suffix={<RadioMark tone="neutral" size="large" />}
value="foo"
/>
</RadioGroup.Root>
</List>
</VStack>
);
}Variants
Highlighted
import { IconPersonCircleLine } from "@karrotmarket/react-monochrome-icon";
import { Box, Icon, VStack } from "@seed-design/react";
import { useState } from "react";
import { List, ListDivider, ListItem } from "seed-design/ui/list";
import { Switch } from "seed-design/ui/switch";
export default function ListHighlighted() {
const [highlighted, setHighlighted] = useState(true);
return (
<VStack width="360px" gap="x4">
<List>
<ListItem
prefix={<Icon svg={<IconPersonCircleLine />} />}
title="하이라이트되지 않은 항목"
detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt."
/>
<ListDivider />
<ListItem
highlighted
prefix={<Icon svg={<IconPersonCircleLine />} />}
title="하이라이트된 항목"
detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt."
/>
</List>
<List>
<ListItem
prefix={<Icon svg={<IconPersonCircleLine />} />}
title="하이라이트"
highlighted={highlighted}
/>
</List>
<Box alignSelf="center">
<Switch
size="24"
tone="neutral"
label="highlight"
checked={highlighted}
onCheckedChange={setHighlighted}
/>
</Box>
</VStack>
);
}With Bottom Sheet
import {
BottomSheetBody,
BottomSheetContent,
BottomSheetFooter,
BottomSheetRoot,
BottomSheetTrigger,
} from "seed-design/ui/bottom-sheet";
import { ActionButton } from "seed-design/ui/action-button";
import { Checkmark } from "seed-design/ui/checkbox";
import { List, ListCheckItem } from "seed-design/ui/list";
import { PrefixIcon, VStack } from "@seed-design/react";
import { useState } from "react";
import { IconArrowClockwiseCircularFill } from "@karrotmarket/react-monochrome-icon";
const TYPES = ["버스", "지하철", "택시", "자전거", "도보"] as const;
export default function ListBottomSheet() {
const [isOpen, setIsOpen] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<(typeof TYPES)[number][]>([]);
return (
<BottomSheetRoot open={isOpen} onOpenChange={setIsOpen}>
<BottomSheetTrigger asChild>
<ActionButton>BottomSheet 열기</ActionButton>
</BottomSheetTrigger>
<BottomSheetContent title="교통수단" description="이동할 교통수단을 선택해주세요.">
<VStack asChild pb="safeArea">
<form
onReset={(e) => {
e.preventDefault();
setSelectedTypes([]);
}}
onSubmit={(e) => {
e.preventDefault();
setIsOpen(false);
}}
>
<BottomSheetBody paddingX="0" asChild>
<List as="fieldset">
{TYPES.map((type) => (
<ListCheckItem
key={type}
title={type}
checked={selectedTypes.includes(type)}
prefix={<Checkmark tone="neutral" size="large" />}
onCheckedChange={() => {
setSelectedTypes((prev) =>
prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type],
);
}}
/>
))}
</List>
</BottomSheetBody>
<BottomSheetFooter>
<VStack gap="x2">
<ActionButton
size="large"
variant="neutralSolid"
disabled={selectedTypes.length === 0}
type="submit"
>
경로 찾기
</ActionButton>
<ActionButton
size="small"
variant="ghost"
disabled={selectedTypes.length === 0}
type="reset"
>
<PrefixIcon svg={<IconArrowClockwiseCircularFill />} />
초기화
</ActionButton>
</VStack>
</BottomSheetFooter>
</form>
</VStack>
</BottomSheetContent>
</BottomSheetRoot>
);
}Customization and Composition
Anatomy
@seed-design/react 패키지에서 제공하는 List와 ListHeader 컴포넌트는 다음과 같은 구조로 사용됩니다.
ListHeader
List.Root
└── List.Item
├── List.Prefix (선택사항)
├── List.Content
│ ├── List.Title
│ └── List.Detail (선택사항)
└── List.Suffix (선택사항)
└── List.Item
├── ...import { ListHeader, List, Icon } from "@seed-design/react";
<ListHeader>
내 정보
</ListHeader>
<List.Root>
<List.Item>
<List.Prefix>
<Icon svg={<IconPersonCircleLine />} />
</List.Prefix>
<List.Content>
<List.Title>내 프로필</List.Title>
<List.Detail>다른 사람들에게 보이는 내 정보를 관리합니다.</List.Detail>
</List.Content>
<List.Suffix>
<Icon svg={<IconArrowRightLine />} />
</List.Suffix>
</List.Item>
<List.Item>
{/* ... */}
</List.Item>
</List.Root>ListHeader: 리스트의 제목이나 설명을 표시하는 헤더 역할List.Root: 모든 리스트 항목을 감싸는 컨테이너 역할List.Item: 개별 리스트 항목. 클릭 가능한 영역을 정의List.Prefix: 아이콘, Avatar, Checkmark 등을 표시할 수 있는 시작 영역List.Content: 주요 콘텐츠가 들어가는 중앙 영역List.Title: 리스트 항목의 제목List.Detail: 부가 설명이나 세부 정보
List.Suffix: 아이콘, Action Button, Toggle Button 등을 표시할 수 있는 끝 영역
asChild prop으로 적절한 시맨틱 요소와 조합하기
Composition
asChild prop에 대해 자세히 알아봅니다.
Using asChild prop in List.Content
리스트 항목 전체 영역을 클릭 가능한 버튼으로 만드는 경우 활용할 수 있는 패턴입니다. 이 경우 List.Item에 asChild prop을 사용하지 않도록 유의하세요. List.Prefix 또는 List.Suffix에 버튼을 넣는 경우 button이 중첩되는 등 유효하지 않은 HTML이 생성됩니다.
import { List as SeedList } from "@seed-design/react";
<SeedList.Item>
<SeedList.Content asChild>
<button type="button" onClick={() => alert("사용자 클릭됨")}>
<SeedList.Title>사용자</SeedList.Title>
</button>
</SeedList.Content>
<SeedList.Suffix>
<ActionButton
size="xsmall"
variant="brandSolid"
onClick={() => alert("보기 클릭됨")}
>
보기
</ActionButton>
</SeedList.Suffix>
</SeedList.Item>Snippet으로 제공되는 ListButtonItem 및 ListLinkItem는 이 패턴을 쉽게 구현할 수 있도록 돕습니다.
import { ListButtonItem } from "seed-design/ui/list";
<ListButtonItem
onClick={() => alert("사용자 클릭됨")}
title="사용자"
detail="항목 6개"
suffix={
<ActionButton
size="xsmall"
variant="brandSolid"
onClick={() => alert("보기 클릭됨")}
>
보기
</ActionButton>
}
/>Using asChild prop in List.Item
리스트 항목 전체 영역을 label로 만들고, List.Prefix 또는 List.Suffix에 Swimarkark, Checkmark 또는 RadioMark를 넣는 경우 활용할 수 있는 패턴입니다.
import { List as SeedList } from "@seed-design/react";
import { Checkbox } from "@seed-design/react/primitive";
<SeedList.Item asChild> {/* <label> */}
<Checkbox.Root defaultChecked>
<SeedList.Content>
<SeedList.Title>동의</SeedList.Title>
</SeedList.Content>
<SeedList.Suffix>
<Checkmark />
</SeedList.Suffix>
<Checkbox.HiddenInput />
</Checkbox.Root>
</SeedList.Item>Snippet으로 제공되는 ListSwitchItem, ListCheckItem 및 ListRadioItem는 이 패턴을 쉽게 구현할 수 있도록 돕습니다.
import { ListCheckItem } from "seed-design/ui/list";
<ListCheckItem
defaultChecked
title="동의"
suffix={<Checkmark size="large" />}
/>Last updated on