세그먼트 컨트롤 선택 UI 9종 — 복붙하면 되는
세그먼트 컨트롤은 iOS 설정 화면에서 자주 보이는, 여러 선택지 중 하나를 고르면 배경 알약이 그 칸으로 미끄러져 가는 탭 전환 버튼입니다. 세그먼트 컨트롤부터 칩 필터·이모지 리액션·컬러 스와치까지, 이름은 몰라도 앱에서 본 적 있는 선택 UI 9종을 코드와 함께 모았습니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 iOS 필 세그먼트 컨트롤
- 02 아이콘 2단 세그먼트 컨트롤
- 03 다중 선택 필터 칩
- 04 사이즈 라디오 칩
- 05 이모지 리액션 팝업 피커
- 06 이모지 버스트 카운트
- 07 그리드 컬러 스와치
- 08 오도미터 시간 스크럽
- 09 체크마크 정렬 드롭다운
아홉 개는 인기 순이 아니라 한 화면에서 겪는 순서로 놓았습니다. 01·02는 여러 화면 중 하나를 고르는 세그먼트 컨트롤이고, 03·04는 칩을 눌러 옵션을 고르는 경우, 05·06은 이모지로 반응을 남기는 경우, 07은 색상을, 08은 시간을, 09는 정렬 기준을 고르는 경우입니다.
01iOS 필 세그먼트 컨트롤
라디오 세 개를 균등한 폭으로 묶고, 파란 알약 배경이 선택된 칸으로 translateX 만큼 옮겨가 그 글자 위에만 얹힙니다. 설정 화면의 보기 모드 전환이나 주문 상태 필터(전체/진행중/완료)에 씁니다.
.seg__pill {
position: absolute;
width: calc((100% - 8px) / 3);
transform: translateX(0);
transition: transform $duration $easing;
}
#seg1-2:checked ~ .seg__track .seg__pill { transform: translateX(100%); }
#seg1-3:checked ~ .seg__track .seg__pill { transform: translateX(200%); }
02아이콘 2단 세그먼트 컨트롤
리스트·그리드 두 아이콘 중 하나를 고르면 필 배경이 트랙 절반 폭만큼 translateX로 옮겨가고, 고른 아이콘만 잉크색으로 짙어집니다. 게시판의 리스트뷰·그리드뷰 전환에 흔히 씁니다.
.iv__pill {
position: absolute;
width: $sp-12;
transform: translateX(0);
transition: transform $duration $easing;
}
#iv1-2:checked ~ .iv__track .iv__pill { transform: translateX(100%); }
03다중 선택 필터 칩
체크박스 여러 개를 칩 모양으로 두고 여러 개를 동시에 고르면, 고른 칩마다 배경이 가운데서부터 scaleX로 채워지고 인라인 SVG 체크 아이콘이 scale(0)에서 1로 커집니다. 쇼핑몰 상품 목록 필터(무료배송/당일배송/할인)에 맞습니다.
.cf__chip::before {
content: "";
position: absolute; inset: 0;
background: $color;
transform: scaleX(0);
transform-origin: center;
transition: transform $duration $easing;
}
.cf__chip:has(.cf__input:checked)::before { transform: scaleX(1); }
04사이즈 라디오 칩
S·M·L·XL 라디오 칩 중 하나를 고르면 그 칩만 scale(.84)로 살짝 눌렸다가 튕겨 돌아오고 테두리 링이 두꺼워집니다. 쇼핑몰 상품 사이즈 선택이나 예약 인원수 선택에 씁니다.
.sz__chip:has(.sz__input:checked) {
border-color: $color;
border-width: 3px;
animation: sz-press $duration $easing;
}
@keyframes sz-press {
0% { transform: scale(1); }
35% { transform: scale(.84); }
70% { transform: scale(1.05); }
100% { transform: scale(1); }
}
05이모지 리액션 팝업 피커
반응 버튼을 누르면 위로 이모지 네 개가 담긴 막대가 opacity·translateY로 떠오르고, 버튼의 aria-expanded 값이 실시간으로 바뀝니다. 메신저 대화나 게시글에 리액션을 남기는 자리에 씁니다.
.rp__pop {
transform: translate(-50%, 8px) scale(.85);
opacity: 0;
pointer-events: none;
transition: transform $duration $easing, opacity $duration $easing;
}
.rp.is-open .rp__pop {
transform: translate(-50%, 0) scale(1);
opacity: 1;
pointer-events: auto;
}
06이모지 버스트 카운트
이모지 하나를 고르면 그 이모지가 scale로 크게 튀었다가 돌아오는 동안 둘레의 점 네 개가 바깥으로 흩어지며 사라지고, 옆 배지 숫자가 1 올라갑니다. 쇼츠·릴스 하트 반응처럼 반응 수를 같이 보여주는 자리에 씁니다.
.bc.is-play .bc__emoji { animation: bc-emoji $duration $easing; }
.bc.is-play .bc__burst { animation: bc-burst $duration $easing; }
@keyframes bc-emoji {
0% { transform: scale(1); }
35% { transform: scale(1.5); }
60% { transform: scale(.9); }
100% { transform: scale(1); }
}
07그리드 컬러 스와치
색상 스와치 6개를 3×2 그리드로 두고 하나를 고르면, 그 칸 오른쪽 아래에 작은 체크 배지가 scale(0)에서 1로 나타납니다. 상품 색상 옵션이나 테마 색상 선택 화면에 맞습니다.
.cw__check {
transform: scale(0);
transition: transform $duration $easing;
}
.cw__check-icon { /* 인라인 Phosphor SVG — currentColor 를 물려받는다 */
position: absolute; inset: 3px;
color: $color;
}
.cw__cell:has(.cw__input:checked) .cw__check { transform: scale(1); }
08오도미터 시간 스크럽
시·분 칸의 위아래 버튼을 누르면 현재 숫자가 translateY로 밀려나가고 다음 숫자가 그 자리로 밀려 들어오며, 자동차 계기판이 넘어가듯 바뀝니다. 예약 시간이나 알람 시각을 정하는 자리에 씁니다.
function roll(col, dir) {
var win = col.querySelector('.tp__win');
var cur = win.querySelector('.is-cur'), next = win.querySelector('.is-next');
var v = (parseInt(cur.textContent, 10) + dir + 60) % 60;
next.textContent = String(v).padStart(2, '0');
cur.classList.add('is-leaving');
next.classList.add('is-arriving');
}
09체크마크 정렬 드롭다운
정렬 버튼을 누르면 옵션 목록이 opacity·translateY로 펼쳐지고, 고른 항목 앞에만 체크마크가 나타나며 트리거 글자도 그 옵션 이름으로 바뀝니다. 상품 목록 정렬(최신순/가격순)이나 게시판 글 정렬에 씁니다.
.sd__menu {
transform: translate(-50%, -6px);
opacity: 0;
transition: transform $duration $easing, opacity $duration $easing;
}
.sd.is-open .sd__menu { transform: translate(-50%, 0); opacity: 1; }
.sd__check { /* 인라인 Phosphor SVG 체크 */
color: $color;
transform: scale(0);
transition: transform $dur-instant $easing;
}
.sd__item.is-checked .sd__check { transform: scale(1); }
어디서 깨지는가 — 숨긴 input과 :has()가 진짜 함정입니다
아홉 개 중 라디오·체크박스로 만든 01·02·03·04·07은 진짜 <input>을 position: absolute; width: 1px; height: 1px; clip: rect(0,0,0,0);로 시각적으로만 숨겼습니다. display: none이나 visibility: hidden을 썼다면 요소가 포커스 순서에서 완전히 빠져 키보드만 쓰는 사람은 Tab으로 만날 수 없었을 것입니다. 03·07이 쓰는 :has()는 Chrome·Edge 105+, Safari 15.4+, Firefox 121+에서만 동작하고, 그 아래 버전에서는 체크 표시가 안 나타날 뿐 클릭 자체는 멀쩡히 됩니다. 아홉 개 파일이 그대로 담긴 zip의 압축 비밀번호는 e9nu36x4이고, 08의 오도미터 롤은 CSS만으로는 두 자리 숫자를 동시에 보여주며 밀어내기 어려워 roll() 자바스크립트 12줄이 텍스트를 바꿔치기하는 타이밍을 잡아 줍니다. 05·09처럼 버튼을 눌러 여는 팝업은 페이지 바깥을 클릭하거나 Esc를 눌러도 닫혀야 하는데, 이 데모는 지면을 아끼려고 Esc만 넣었고 바깥 클릭 닫기는 뺐습니다 — 실제로 붙일 때는 document 클릭 리스너를 하나 더 추가하는 편이 안전합니다.
접근성
01·02·03·04·07은 실제 <input>을 숨기되 지우지 않아 스크린 리더가 라디오·체크박스 그대로 읽고, Tab과 Space·화살표 키만으로 상태를 바꿀 수 있습니다. 05·09의 팝업·메뉴 트리거는 aria-expanded 값을 열고 닫을 때마다 갱신하고, 09의 각 옵션은 role="menuitemradio"와 aria-checked로 현재 선택을 알립니다. 06의 카운트는 aria-live="polite"라 반응 수가 바뀔 때 스크린 리더가 조용히 새 값을 읽어 줍니다. prefers-reduced-motion: reduce에서는 아홉 개 전부 트랜지션·애니메이션을 껐고, 필·체크·메뉴는 켜지거나 닫힌 최종 모양 그대로 남아 상태 정보 자체는 사라지지 않습니다.
@media (prefers-reduced-motion: reduce) {
.seg__pill, .cf__chip::before, .cw__check { transition: none; }
.bc.is-play .bc__emoji, .bc.is-play .bc__burst { animation: none !important; }
}
선택 UI 말고 다른 CSS 애니메이션 소재는 실전 UI 카테고리에 더 있고, 이 사이트가 데모를 어떤 기준으로 검증하는지는 소개 페이지에 적어 뒀습니다.
FAQ
세그먼트 컨트롤과 탭은 뭐가 다른가요?
탭은 눌렀을 때 보통 다른 페이지나 다른 콘텐츠 영역으로 이동하고, 세그먼트 컨트롤은 같은 화면 안에서 필터나 보기 방식처럼 값 하나만 바꿉니다. 01·02가 페이지 전환 없이 즉시 상태만 바꾸는 이유입니다.
:has()를 못 쓰는 브라우저에서는 03·04·07이 완전히 망가지나요?
아니요. :has()가 없으면 체크·링 표시만 안 보일 뿐, 실제 <input>의 :checked 상태와 폼 제출 값은 그대로 정상 동작합니다. 시각적 피드백만 못 받을 뿐 기능 자체는 멀쩡합니다.
React판에서도 09의 메뉴가 Esc로 닫히나요?
네, react/09-sort-dropdown-checkmark-menu의 컴포넌트에도 같은 onKeyDown 리스너가 들어 있어 Esc를 누르면 open 상태가 false로 바뀌며 닫힙니다. 다만 바깥 클릭으로 닫는 처리는 함정 섹션에 적었듯 이 데모에는 빠져 있어 실제 프로젝트에 붙일 때 추가하는 걸 권합니다.