Skip to content

세그먼트 (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>

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.html include 입니다.
  • 활성 클래스는 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 씩이라 아무 문제가 없습니다. 좁은 화면에서만 드러나는 고장이에요.

그래서 항목을 늘리거나 라벨을 길게 쓸 때는 가장 좁은 화면에서 눈으로 확인하는 것이 유일한 방법입니다. 랜딩이 그때 쓴 대처는 이렇습니다.

  1. 라벨을 줄인다 — 모바일에서 축약형으로 바꿉니다(정상 해법)
  2. 좌우 패딩을 좁힌다 — fill 상태에서 패딩은 「최소 내용 폭」으로만 작동하므로, 좁히면 여유가 생깁니다
  3. overflow-x: auto 를 열어 둔다 — 넘쳐도 최소한 닿을 수는 있게 하는 보험입니다

3번은 어디까지나 안전망입니다. 정상 해법은 여전히 1번이에요.

앱에서 크기를 고칠 때 ​

공유 design-system.css 를 건드리면 전 앱 세그먼트가 같이 바뀝니다. 앱 사정으로 패딩을 조절해야 하면 그 화면에만 스코프해 재선언하세요 — 랜딩의 실제 예입니다.

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 (기본)28px8px 12px13px / 5002px / 4px.ds-segment (modifier 없음)
md36px12px 16px14px / 5004px / 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 를 둘 다 사이즈해야 한다」인데, 이 슬롯은 둘 다 없습니다. 쓰기로 한다면 그 규칙부터 채워야 합니다.

호출 패턴 ​

html
<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 로 넘깁니다.

ts
get documentSegmentTabs(): SegmentItem[] {
  return [
    { id: "submit", label: "서류 제출" },
    { id: "request", label: "서류 요청" },
  ];
},

클래스를 직접 써도 되나 ​

항목이 고정 2개이고 반응형 목록이 아니면 .ds-segment / .ds-segment__item 을 직접 써도 됩니다. TabNav·SubTab 과 달리 「마크업을 손으로 짓지 말라」는 강제는 없습니다 — 실제로 급여 시트의 셀 상세 패널이 그렇게 쓰고 있습니다.

html
<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 도 함께 바뀝니다
fillbooleanfalsetrue 면 width: 100% + 항목 flex: 1

SegmentItem (탭 1개) ​

필드타입필수설명
idstring✅onSelect 에 넘어가는 고유 id
labelstring✅탭 텍스트
leadingIconstring라벨 앞 아이콘 이름 (레지스트리). ⚠️ 지금은 0×0 이라 안 보입니다 (이유)
trailingIconstring라벨 뒤 아이콘 이름. ⚠️ 같음
disabledboolean클릭 무시 + 흐리게 + 포커스 제외

컴포넌트가 노출하는 멤버 (파셜이 사용) ​

멤버설명
segmentGroupClass그룹 모디파이어("" / "ds-segment--md" / "ds-segment--fill" 조합) (getter)
segmentItemsgetTabs() 결과 (getter)
segmentActiveIdgetActive() 결과 (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)
Geometrysm: 항목 28px · 패딩 8px 12px · 트랙 패딩 2 / radius 4 — md: 항목 36px · 패딩 12px 16px · 트랙 패딩 4 / radius 8
Motion0.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 partialpackages/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-08Segment 페이지 신규 작성 — Playground(size · fill · 항목 수 · 아이콘 · disabled · 좁은 폭) + 모아보기 7상태. 그전에는 SubTab 문서의 비교 tip 에만 언급돼 있었습니다