SEED Design

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:list

Anatomy

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

ListHeaderList 밖에 위치합니다.

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와 같은 컨트롤 요소를 prefixsuffix 영역에 넣어 사용합니다.

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>입니다. ListCheckItemListRadioItem를 사용하는 경우 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 패키지에서 제공하는 ListListHeader 컴포넌트는 다음과 같은 구조로 사용됩니다.

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.ItemasChild 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으로 제공되는 ListButtonItemListLinkItem는 이 패턴을 쉽게 구현할 수 있도록 돕습니다.

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.SuffixSwimarkark, 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, ListCheckItemListRadioItem는 이 패턴을 쉽게 구현할 수 있도록 돕습니다.

import { ListCheckItem } from "seed-design/ui/list";

<ListCheckItem
  defaultChecked
  title="동의"
  suffix={<Checkmark size="large" />}
/>

Last updated on