세그먼트 (Segment)
회색 통 안에서 흰 pill 이 옮겨 다니는 iOS 형태의 전환 컨트롤입니다. 크기·균등 분할을 확인하고, 폭이 모자랄 때 탭이 조용히 사라지는 것을 직접 만들어 보세요.
Alpine 컴포넌트 · Web · 진행 중(WIP) · 문서 포맷 시범 적용
Interactive playground
.ds-segment* 클래스로 렌더해요 — 항목을 눌러 흰 pill 이 옮겨 가는 것을 확인하세요. 「좁은 폭」(160px)을 켜고 항목 수를 늘려 보세요 — 2개까지는 멀쩡하다가 4개에서 라벨이 잘려 나갑니다..ds-segment 가 overflow: hidden 이라 스크롤도 안 생겨요. (잘림은 fill 을 켠 자리에서만 나므로 함께 켜집니다 — 기본 트랙은 inline-flex 라 잘리는 대신 부모를 넘어갑니다.) 「앞/뒤 아이콘」을 켜도 아무것도 안 보이는 것이 현재 동작입니다 — 실측 0×0. // 탭 목록은 호스트가 갖는다 — 반응형 source 라 getter 로 넘긴다
const myTabs = [
{ id: 't0', label: '서류 제출' },
{ id: 't1', label: '서류 요청' },
];
<div x-data="segment({
getTabs: () => myTabs, // () => SegmentItem[]
getActive: () => activeId, // () => string
onSelect: (id) => switchTo(id), // (id) => void
})">
<!-- include "../../packages/components/html/segment.html" -->
</div>sm — 기본. 28px · 라벨 13px · 트랙 radius 4
md — 36px · 라벨 14px · 트랙 radius 8. 유일한 실제 사용처(직원앱 서류 업로드)가 이것 + fill 입니다
fill — 컨테이너를 균등 분할. 폭이 없으면 --fill 은 의미가 없어 320px 안에 뒀습니다
3개 이상 — 항목 수 제한은 없습니다. 다만 늘어날수록 아래 잘림 위험이 커집니다
disabled — 클릭 무시 + 흐림. 실제로 쓰는 곳은 아직 없습니다
⚠️ 아이콘 — leadingIcon: 'ic-document' 을 넘긴 상태입니다. 아무것도 안 보이는 게 맞습니다 — 실측 0×0 (아래 참고)
⚠️ 랜딩에서 사고가 났던 조합 — 긴 라벨 4탭 + md + fill 을 240px 안에. 항목은 58px 인데 라벨이 64px 이라 옆 탭 위로 삐져나와 겹칩니다
같은 4탭을 fill 없이 240px 안에 — 이때는 겹치는 게 아니라 트랙이 342px 로 부모를 넘어갑니다(자르는 건 트랙이 아니라 부모 쪽)
같은 4탭이 폭만 넉넉하면(560px) 아무 문제 없습니다 — 그래서 넓은 화면에서만 보면 사고를 못 잡습니다
size · fill 과 탭 단위 슬롯(앞/뒤 아이콘 · disabled)을 켜고 끄면 실제 .ds-segment* 클래스로 렌더된 결과와 호출 코드가 함께 바뀝니다. 「좁은 폭」은 옵션이 아니라 놓이는 자리를 좁히는 것이고, 이 컴포넌트에서 가장 자주 터지는 상황입니다.
규칙 (강제) · 코딩 에이전트용
Segment 강제 규칙. 상세는 아래 본문.
⚠️ 트랩 ① 폭이 모자라면 조용히 무너집니다 (실제로 터졌습니다).ds-segment 는 overflow: hidden 이고 .ds-segment__item 은 white-space: nowrap 입니다. 스크롤도 안 생기고, 에러도 없고, 빌드도 lint 도 green 입니다. fill 을 켠 자리에서는 항목이 균등하게 줄다가 라벨이 옆 탭 위로 삐져나와 겹치고 트랙 밖은 잘리며, fill 없이는 트랙이 부모를 넘어갑니다(실측값은 폭 절). 랜딩에서 이 상태로 4탭 중 3탭이 모바일에서 죽어 있었습니다. 라벨을 늘리거나 항목을 추가할 때는 가장 좁은 화면에서 반드시 눈으로 확인하세요 — 넓은 화면에서는 절대 안 드러납니다.
⚠️ 트랩 ② 항목 버튼에 Tailwind border-* 유틸을 붙이지 마세요design-system.css 의 button[class*="border"] 규칙이 4px radius 를 !important 로 되살립니다. 테두리는 .ds-segment__item 이 이미 갖고 있으니 더할 필요가 없습니다. (SubTab 과 같은 함정입니다.)
⚠️ 트랩 ③ 아이콘 슬롯은 지금 동작하지 않습니다 (0×0 · 실측)leadingIcon / trailingIcon 에 이름을 넘겨도 아무것도 안 보입니다. 파셜이 <span class="inline-flex items-center"> 안에 등록본 SVG 를 넣는데 파셜에도 CSS 에도 크기 선언이 없고, 등록본 SVG 는 viewBox 만 있고 width/height 가 없습니다. 그래서 래퍼 span 과 SVG 가 둘 다 0×0 으로 접힙니다(2026-09-08 실측). 에러도 경고도 없습니다. 프로덕션에서 이 슬롯을 쓰는 곳이 한 곳도 없어서 드러나지 않았을 뿐이니, 쓰려면 CSS 규칙을 먼저 채워야 합니다.
⚠️ 트랩 ④ SubTab 과 다른 컴포넌트입니다 세그먼트는 하나의 회색 통 안에서 흰 pill 이 옮겨 다니고 항상 하나가 선택돼 있습니다. SubTab 은 낱개 탭이 간격을 두고 나열되고 선택 없음("")도 정상 상태입니다. 겉모습만 보고 바꿔 쓰지 마세요.
마크업
- 기본은
x-data="segment({...})"+packages/components/html/segment.htmlinclude 입니다. - 활성 클래스는
is-active입니다(SubTab 과 같고, TabNav 의tab-active와 다릅니다). - 옵션:
size(기본sm) ·fill(기본false).
언제 쓰나
같은 대상을 다른 각도로 보는 전환에 씁니다. 화면이 바뀌는 게 아니라 한 영역의 내용만 갈아 끼우는 자리예요.
- ✅ 서류 업로드 모달에서 「서류 제출 ↔ 서류 요청」
- ✅ 셀 상세 패널에서 「셀 상세 ↔ 메모」
- ❌ 페이지 자체가 바뀌는 상단 탭 → TabNav
- ❌ 상위 탭 아래 하위 분류 → SubTab
셋을 고르는 기준
| 생김새 | 선택 없음 | 쓰는 자리 | |
|---|---|---|---|
| Segment | 회색 통 + 흰 pill | 불가 — 항상 하나 | 한 영역 안의 뷰 전환 |
| SubTab | 낱개 탭이 간격을 두고 | 가능("") | 상위 탭 아래 하위 분류 |
| TabNav | 밑줄형 | 불가 | 페이지 전환 |
폭이 이 컴포넌트의 전부입니다
잘린 탭에는 닿을 방법이 없습니다 — 스크롤도, 에러도 없습니다
.ds-segment 는 overflow: hidden, .ds-segment__item 은 white-space: nowrap 입니다. 둘이 합쳐지면 트랙보다 내용이 넓어진 순간 넘친 부분이 잘려 나가고 닿을 방법이 없습니다.
랜딩에서 실제로 4탭 중 3탭이 모바일에서 도달 불가가 된 적이 있습니다. 빌드도 lint 도 green 이라 아무 신호가 없었어요.
어떻게 망가지는지는 fill 이 정합니다. 둘을 헷갈리면 「우리 화면은 괜찮다」고 잘못 판단하게 됩니다. 아래는 이 페이지 데모를 실제로 재 본 값입니다(2026-09-08).
| 상태 | 트랙 폭 | 좁은 자리에 두면 | 실측 |
|---|---|---|---|
기본 (inline-flex) | 내용만큼 | 부모를 넘어갑니다. 트랙 자신은 안 자르므로 잘라내는 건 부모 쪽이고, 안 자르는 부모면 옆 요소를 밀어냅니다 | 240px 자리에 4탭 → 트랙이 342px 로 부모 밖으로 |
fill: true (flex + width: 100%) | 부모에 묶임 | 항목이 flex: 1 로 균등하게 줄고, 라벨은 nowrap 이라 안 줄어듭니다. 라벨이 항목보다 넓어지면 옆 탭 위로 삐져나와 겹치고, 트랙 가장자리를 넘는 부분은 잘립니다 | 160px 자리에 4탭 → 항목 39px vs 라벨 48px, 첫 탭 라벨이 트랙 왼쪽에서 잘림 |
즉 fill 을 켠 자리가 위험합니다. 직원앱 서류 업로드 · 셀 상세 패널 · 랜딩 데모가 전부 fill 입니다. (문서 위 Playground 의 「좁은 폭」이 fill 을 함께 켜는 것도 이 때문입니다 — 안 켜면 잘림이 재현되지 않습니다. 항목 수를 2 → 4 로 올려 보세요.)
넓은 화면에서만 보면 절대 못 잡습니다
같은 4탭도 폭이 560px 이면 항목이 138px 씩이라 아무 문제가 없습니다. 좁은 화면에서만 드러나는 고장이에요.
그래서 항목을 늘리거나 라벨을 길게 쓸 때는 가장 좁은 화면에서 눈으로 확인하는 것이 유일한 방법입니다. 랜딩이 그때 쓴 대처는 이렇습니다.
- 라벨을 줄인다 — 모바일에서 축약형으로 바꿉니다(정상 해법)
- 좌우 패딩을 좁힌다 —
fill상태에서 패딩은 「최소 내용 폭」으로만 작동하므로, 좁히면 여유가 생깁니다 overflow-x: auto를 열어 둔다 — 넘쳐도 최소한 닿을 수는 있게 하는 보험입니다
3번은 어디까지나 안전망입니다. 정상 해법은 여전히 1번이에요.
앱에서 크기를 고칠 때
공유 design-system.css 를 건드리면 전 앱 세그먼트가 같이 바뀝니다. 앱 사정으로 패딩을 조절해야 하면 그 화면에만 스코프해 재선언하세요 — 랜딩의 실제 예입니다.
/* 공유 CSS 대신 데모 섹션에만 스코프한다 */
#demo .ds-segment--md .ds-segment__item {
padding-inline: var(--ds-spacing-28);
}앱 CSS 는 @import 뒤라 소스 순서로 이미 이깁니다 — !important 를 쓰지 마세요.
접근성
- 그룹에
role="tablist", 항목에role="tab"+aria-selected가 붙습니다(파셜이 넣습니다). - 비활성 항목은
disabled속성이라 키보드 포커스에서도 빠집니다.
크기 (size)
두 단계이고, segment({ size }) 로 지정합니다(기본 sm). 트랙의 패딩·radius 까지 함께 바뀝니다 — 항목만 커지는 것이 아니에요.
| token | 항목 높이 | 항목 패딩 | 라벨 | 트랙 패딩 / radius | 클래스 |
|---|---|---|---|---|---|
sm (기본) | 28px | 8px 12px | 13px / 500 | 2px / 4px | .ds-segment (modifier 없음) |
md | 36px | 12px 16px | 14px / 500 | 4px / 8px | .ds-segment--md |
균등 분할 (fill)
기본은 inline-flex 라 내용만큼만 넓습니다. fill: true 를 주면 display: flex; width: 100% 가 되고 항목이 flex: 1 로 균등 분할됩니다.
fill 은 「폭이 정해진 자리」에서만 의미가 있습니다
부모에 폭이 없으면 width: 100% 가 기댈 것이 없어 결과가 달라 보이지 않습니다. 모달·패널처럼 폭이 확정된 컨테이너 안에서 쓰세요.
그리고 fill 일 때 패딩의 역할이 바뀝니다 — 폭은 화면이 정하므로 패딩은 「최소 내용 폭」으로만 작동합니다. 그래서 좁은 화면에서 잘림이 날 때 패딩을 좁히는 것이 통합니다.
탭 단위 슬롯
| 슬롯 | 동작 | 실제 사용 |
|---|---|---|
leadingIcon | 라벨 앞 아이콘 (레지스트리 이름) | 없음 · ⚠️ 지금은 0×0 이라 안 보입니다 |
trailingIcon | 라벨 뒤 아이콘 | 없음 · ⚠️ 같음 |
disabled | 클릭 무시 + 흐림 + 포커스 제외 | 없음 (동작은 정상) |
셋 다 구현돼 있지만 프로덕션에서 쓰는 곳이 한 곳도 없습니다. 그중 아이콘 둘은 켜도 화면에 안 나옵니다 — 쓰기 전에 아래를 보세요.
아이콘 슬롯은 지금 동작하지 않습니다
이름을 넘겨도 아이콘이 안 보입니다 — 0×0 으로 접힙니다
leadingIcon: 'ic-document' 를 넘기고 실제로 렌더해 재 본 결과입니다(2026-09-08).
| 잰 것 | 값 |
|---|---|
래퍼 <span class="inline-flex items-center"> | 0 × 0 |
그 안의 <svg> | 0 × 0 (getComputedStyle 도 0px / 0px) |
에러도 경고도 없습니다. 옵션을 켠 사람은 「아이콘 이름을 잘못 썼나」 하고 헤매게 됩니다. 모아보기 탭의 아이콘 칸이 비어 보이는 것도 이 때문이고, 그게 현재 동작이 맞습니다.
원인은 크기를 아무도 안 주기 때문입니다. 파셜은 래퍼 span 에 inline-flex items-center 만 주고 크기 클래스를 안 붙이며, design-system.css 의 .ds-segment__item 에도 자식 SVG 를 재는 규칙이 없습니다. 등록본 SVG 는 viewBox 만 있고 width/height 속성이 없어 기댈 값이 하나도 없습니다.
디자인 시스템의 아이콘 규칙은 「새 아이콘 슬롯은 X > svg 와 X > .ds-ico 를 둘 다 사이즈해야 한다」인데, 이 슬롯은 둘 다 없습니다. 쓰기로 한다면 그 규칙부터 채워야 합니다.
호출 패턴
<div x-data="segment({
getTabs: () => documentSegmentTabs, // () => SegmentItem[]
getActive: () => documentMode, // () => string
onSelect: (id) => switchDocumentMode(id),
size: 'md', // 'sm'(기본 28px) | 'md'(36px) — 선택
fill: true, // 균등 분할 — 선택
})">
<!-- include "../../packages/components/html/segment.html" -->
</div>탭 목록은 호스트가 갖습니다. 반응형 source 라 배열이 아니라 getter 로 넘깁니다.
get documentSegmentTabs(): SegmentItem[] {
return [
{ id: "submit", label: "서류 제출" },
{ id: "request", label: "서류 요청" },
];
},클래스를 직접 써도 되나
항목이 고정 2개이고 반응형 목록이 아니면 .ds-segment / .ds-segment__item 을 직접 써도 됩니다. TabNav·SubTab 과 달리 「마크업을 손으로 짓지 말라」는 강제는 없습니다 — 실제로 급여 시트의 셀 상세 패널이 그렇게 쓰고 있습니다.
<div class="ds-segment ds-segment--fill min-w-0 flex-1" role="tablist">
<button type="button" role="tab" class="ds-segment__item"
:class="detailPanelTab === 'cell' ? 'is-active' : ''"
:aria-selected="detailPanelTab === 'cell'"
@click="setDetailPanelTab('cell')">셀 상세</button>
…
</div>직접 쓸 때 놓치기 쉬운 것 둘입니다.
role="tablist"·role="tab"·:aria-selected를 직접 붙여야 합니다 — 파셜이 해 주던 일입니다.border-*유틸을 붙이지 마세요 —button[class*="border"]가 4px radius 를!important로 되살립니다.
API
segment(opts: SegmentOptions) Alpine.data factory.
SegmentOptions (모두 optional)
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
getTabs | () => SegmentItem[] | — | 반응형 탭 목록 source |
getActive | () => string | — | 반응형 활성 탭 id source |
onSelect | (id: string) => void | — | 비활성 아닌 탭 클릭 시 id로 호출 |
size | 'sm' | 'md' | 'sm' | 28px / 36px. 트랙 패딩·radius 도 함께 바뀝니다 |
fill | boolean | false | true 면 width: 100% + 항목 flex: 1 |
SegmentItem (탭 1개)
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
id | string | ✅ | onSelect 에 넘어가는 고유 id |
label | string | ✅ | 탭 텍스트 |
leadingIcon | string | 라벨 앞 아이콘 이름 (레지스트리). ⚠️ 지금은 0×0 이라 안 보입니다 (이유) | |
trailingIcon | string | 라벨 뒤 아이콘 이름. ⚠️ 같음 | |
disabled | boolean | 클릭 무시 + 흐리게 + 포커스 제외 |
컴포넌트가 노출하는 멤버 (파셜이 사용)
| 멤버 | 설명 |
|---|---|
segmentGroupClass | 그룹 모디파이어("" / "ds-segment--md" / "ds-segment--fill" 조합) (getter) |
segmentItems | getTabs() 결과 (getter) |
segmentActiveId | getActive() 결과 (getter) |
segmentIsActive(id) | 활성 여부 |
segmentSelect(item) | disabled면 무시, 아니면 onSelect(item.id) |
segmentIconSvg(name) | 아이콘 이름 → SVG 문자열 (이름이 없으면 "") |
사용된 토큰
| 카테고리 | 토큰 |
|---|---|
| Color | --color-label-disable(트랙 배경, 20% 혼합) · --color-label-assistive(비활성 라벨) · --color-label-normal(활성·hover 라벨) · --color-label-white-normal(활성 pill 배경) · --color-label-muted(disabled) |
| Geometry | sm: 항목 28px · 패딩 8px 12px · 트랙 패딩 2 / radius 4 — md: 항목 36px · 패딩 12px 16px · 트랙 패딩 4 / radius 8 |
| Motion | 0.12s ease — background · color · box-shadow |
토큰화가 안 끝난 값이 있습니다
활성 pill 의 그림자가 rgba(7, 7, 7, 0.06) 인라인이고 (elevation 토큰이 아닙니다), 항목 높이·폰트·radius 도 숫자로 직접 적혀 있습니다. 아래 검토 필요 사항 참고.
사용처
| 위치 | 방식 | 조합 |
|---|---|---|
| 직원 앱 — 서류 업로드 모달 | segment({...}) 컴포넌트 | md + fill, 2탭, 아이콘 없음 |
| 급여 시트 — 셀 상세 패널 | .ds-segment 클래스 직접 | sm + fill, 2탭 |
| 랜딩 — 데모 섹션 | 클래스 직접 + 앱 CSS 로 패딩 재선언 | md, 4탭 |
컴포넌트를 정식으로 부르는 곳은 직원 앱 한 곳뿐입니다.
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/Segment.ts |
| HTML partial | packages/components/html/segment.html |
| 스타일 | packages/static/styles/design-system.css (.ds-segment*) |
| Alpine 등록 | segment (plugin.ts) |
| 원본 디자인 | Figma Tab Segment 10253:9760 / 10253:9761 |
검토 필요 사항
overflow: hidden이 잘림을 조용하게 만듭니다. 넘친 탭이 스크롤도 없이 사라지고 아무 신호가 없습니다 — 랜딩에서 실제로 4탭 중 3탭이 죽었습니다. 기본값을overflow-x: auto로 바꿀지(넘쳐도 닿기는 함), 아니면 지금처럼 두고 화면마다 확인할지 결정이 필요합니다.- 활성 pill 의 그림자가 토큰이 아닙니다 —
box-shadow: 0 4px 4px rgba(7,7,7,0.06), 0 0 8px rgba(7,7,7,0.06)인라인입니다. elevation 토큰으로 옮길지 검토가 필요합니다. - 크기·폰트·radius 가 숫자로 직접 적혀 있습니다 —
28px·13px·4px등. SubTab·TabNav 과 같은 상태이고, 마이크로 사이즈 토큰 정책이 정해지면 함께 옮길 대상입니다. - 아이콘 슬롯이 동작하지 않습니다 — 0×0 실측 (위). 옵션은 있는데 켜면 아무것도 안 나오고 에러도 없습니다. 프로덕션에서 아직 안 쓰여 드러나지 않았을 뿐입니다. 고치려면
.ds-segment__item의 자식 아이콘을 재는 규칙(> span > svg와.ds-ico를 둘 다)을 넣거나, 파셜의 래퍼를.ds-ico+ 크기 클래스로 바꿔야 합니다. 문서만으로 정할 일이 아니라 여기에 남깁니다. - 쓰는 법이 두 가지로 갈려 있습니다 — 컴포넌트 호출 1곳, 클래스 직접 2곳. TabNav·SubTab 처럼 「마크업을 손으로 짓지 말 것」으로 강제할지, 지금처럼 둘 다 허용할지 정해지지 않았습니다.
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-09-08 | Segment 페이지 신규 작성 — Playground(size · fill · 항목 수 · 아이콘 · disabled · 좁은 폭) + 모아보기 7상태. 그전에는 SubTab 문서의 비교 tip 에만 언급돼 있었습니다 |