List
여러 Item을 같은 정렬축과 divider 규칙으로 묶는 그룹 primitive.
List는 Item의 모음이다. 역할은 단순하다. item들을 세로로 쌓되, content 시작선, actions 끝선, divider inset, surface radius를 한 번에 통제한다.
Tiro에서 설정 화면이 안정적으로 보이려면 개별 row보다 List가 더 중요하다. Granola 같은 화면도 결국은 “예쁘게 만든 한 카드”라기보다, 같은 문법의 item들이 잘 정렬된 list들의 연속에 가깝다.
Mobile
List 자체의 시각·정렬 명세는 플랫폼 분기 없음 — web과 동일하게 적용. 각 row의 터치 타겟·gesture·hit area 가이드는 Item Mobile에서 다룬다. row 사이 separator의 inset 처리는 Separator Mobile 참조.
Preview
같은 섹션 안에서는 모든 row가 같은 시작선과 끝선을 공유해야 한다. 섹션 라벨은 조용한 보조 텍스트로, 카드는 정렬 시스템으로만 작동한다. 카드 자체에 헤더를 붙이지 않고, 섹션 라벨은 카드 외부에 둔다.
Anatomy
List
├─ ListHeader (optional)
├─ Item
├─ ListSeparator
├─ Item
└─ ListFooter (optional)List는 새 row 타입을 만들지 않는다. row는 항상 Item이다.
Variants
plain— 배경/테두리 없음. 페이지 위에 바로 놓는 기본형surface— white surface + border + rounded corners. settings와 summary의 기본형muted—stone-50기반. 보조 그룹이나 덜 강조된 목록
Layout Invariants
- 같은 list 안의 모든 item은 같은 content 시작선을 가져야 한다
- trailing control은 같은 끝선에 정렬한다
- separator inset은 surface padding과 맞춘다
- item별 개별 border/radius는 만들지 않는다
- 시각 grouping은 item이 아니라 list가 책임진다
이 규칙이 깨지면 화면은 바로 산만해진다. List는 “row들의 카드 묶음”이 아니라 정렬 시스템이라고 보는 편이 맞다.
Interaction Model
한 list 안에서는 가능하면 하나의 상호작용 모델을 유지한다.
| Good | Less good |
|---|---|
| switch rows만 모은 list | navigation row, switch row, destructive action row를 무작위로 섞은 list |
| select rows만 모은 list | dense form field와 summary row를 같은 문법 없이 섞은 list |
| summary rows만 모은 list | 길고 설명적인 prose block을 list 안에 집어넣는 것 |
섞어야 한다면 섹션을 나누거나 list를 분리한다.
Root Semantics
| Case | Recommended root |
|---|---|
| 구조적 목록 | ul + li |
| 설정 그룹 | div |
| 순서가 중요한 단계 목록 | ol |
List 자체는 behavior primitive가 아니므로 Radix root가 필요하지 않다.
Token Mapping
| Token | Value |
|---|---|
| surface bg | #FFFFFF |
| muted bg | stone-50 |
| surface border | 1px solid stone-100 |
| surface radius | rounded-xl |
| horizontal inset | px-5 |
| row spacing | py-2.5 |
| divider style | 1px dashed stone-100 |
| divider inset | mx-5 |
| section gap | space-y-10 |
Size
| Size | Use |
|---|---|
default | settings, calendar, integrations, team management |
compact | side panels, short utility lists |
Compact는 divider와 padding을 줄이되, title/description 타이포는 함부로 축소하지 않는다.
Usage
<List variant="surface">
<Item>
<ItemContent>
<ItemTitle>Added to folder</ItemTitle>
<ItemDescription>
Get notified when you're added to a new folder.
</ItemDescription>
</ItemContent>
<ItemActions>
<Select value={value} onValueChange={setValue} />
</ItemActions>
</Item>
<Item>
<ItemContent>
<ItemTitle>Note shared with you</ItemTitle>
<ItemDescription>
Get notified when someone shares a note directly with you.
</ItemDescription>
</ItemContent>
<ItemActions>
<Select value={value} onValueChange={setValue} />
</ItemActions>
</Item>
</List><List variant="surface" as="ul">
<Item as="li">
<ItemMedia variant="swatch" />
<ItemContent>
<ItemTitle>lucy@theplato.io</ItemTitle>
</ItemContent>
<ItemActions>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</ItemActions>
</Item>
</List>Form Fields in List
TextInput in Item row
ItemActions 안에 TextInput size="sm"을 배치해 inline 폼 row를 만든다. 상세 패턴은 Item 문서 참조.
Textarea in List
Textarea는 자체 border를 가지므로 List surface 카드 안에 넣으면 이중 박스처럼 보일 수 있다. 하지만 같은 섹션 안에 Item row와 Textarea를 함께 묶어야 할 때는 아래 패턴을 사용한다.
Item row + Textarea 혼합 (같은 주제를 묶을 때)
<List variant="surface">
<Item>
<ItemContent>
<ItemTitle>회사명</ItemTitle>
</ItemContent>
<ItemActions>
<TextInput size="sm" className="w-[200px]" value={name} onChange={...} />
</ItemActions>
</Item>
<ListSeparator />
<div className="px-5 py-3">
<ItemTitle className="mb-2">회사 소개</ItemTitle>
<Textarea value={description} onChange={...} placeholder="..." />
</div>
</List>div className="px-5 py-3"— List의px-5inset과 맞춰 content 시작선을 통일Textarea를 Item 슬롯에 넣지 않는다 — 자유 블록(free block)으로 배치
항목 추가 폼 (Add Form)
List에 새 항목을 추가하는 form이 필요할 때, form을 List surface 카드 안에 넣지 않는다.
List surface 카드는 이미 border를 갖고 있다. 그 안에 TextInput 2개 이상 + 버튼을 가로로 나열하면 border가 겹쳐 "박스 안에 박스 여러 개" 패턴이 만들어지고, 화면이 시각적으로 무거워진다.
올바른 패턴: form을 List 외부에 분리
{/* ✅ 추가 form은 List 위에 독립 배치 */}
<form className="flex gap-2">
<TextInput placeholder="이름" className="flex-1" />
<TextInput placeholder="이메일" className="flex-1" type="email" />
<Button type="submit" size="icon"><Plus /></Button>
</form>
<List variant="surface">
<Item>...</Item>
<ListSeparator />
<Item>...</Item>
</List>- form은 List와 형제(sibling) 요소로 배치한다
- form 자체에 surface 카드를 씌우지 않는다 — input들의 border만으로 폼 영역이 충분히 인식된다
- 입력 필드 1개라면
ItemActions에TextInput size="sm"을 넣는 Item inline 패턴을 사용할 수 있다
피해야 할 패턴
{/* ❌ surface 카드 안에 multi-input form → 이중 박스 노이즈 */}
<List variant="surface">
<div className="p-4">
<TextInput placeholder="이름" />
<TextInput placeholder="이메일" />
<Button><Plus /></Button>
</div>
<ListSeparator />
<Item>...</Item>
</List>Textarea 단독 섹션 (Item row 없이)
같은 섹션에 Item row가 없고 Textarea만 있다면, surface 카드 없이 plain 배치하거나 variant="plain" List를 사용한다. Textarea 하나만을 위해 surface 카드로 감싸면 이중 박스처럼 보인다. 다만 전체 페이지 리듬 상 카드가 필요한 경우에는 div className="px-5 py-3" 패턴으로 배치한다.
{/* ✅ surface 카드 안의 standalone textarea */}
<List variant="surface">
<div className="px-5 py-3">
<Textarea value={context} onChange={...} placeholder="..." />
</div>
</List>Do / Don't
- Do:
List를 item들의 정렬과 리듬을 통제하는 primitive로 본다 - Do: button, switch, select는 각 컴포넌트 문서의 사이즈와 스타일 규칙을 그대로 사용한다
- Do: settings 화면에서 같은 의미의 rows를 같은 list에 묶는다
- Do: Textarea를 List 안에 넣을 때는
div className="px-5 py-3"자유 블록으로 배치해 inset을 맞춘다 - Don't: list를 큰 카드 장식으로 취급해 그림자와 과한 padding을 추가하지 않는다
- Don't: item 하나하나에 개별 rounded border를 반복한다
- Don't: raw HTML form control을 브라우저 기본 스타일로 그대로 넣지 않는다
- Don't:
Textarea를Item의 슬롯(ItemActions등) 안에 넣지 않는다 - Don't: bordered
TextInput2개 이상을 List surface 카드 안에 나란히 배치하지 않는다 — 이중 박스 노이즈가 생긴다. 항목 추가 form은 List 외부에 분리한다
