GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

세그먼트 컨트롤 선택 UI 9종 — 복붙하면 되는

세그먼트 컨트롤은 iOS 설정 화면에서 자주 보이는, 여러 선택지 중 하나를 고르면 배경 알약이 그 칸으로 미끄러져 가는 탭 전환 버튼입니다. 세그먼트 컨트롤부터 칩 필터·이모지 리액션·컬러 스와치까지, 이름은 몰라도 앱에서 본 적 있는 선택 UI 9종을 코드와 함께 모았습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기 순이 아니라 한 화면에서 겪는 순서로 놓았습니다. 01·02는 여러 화면 중 하나를 고르는 세그먼트 컨트롤이고, 03·04는 필터 칩 UI를 눌러 옵션을 고르는 경우, 05·06은 이모지로 반응을 남기는 경우, 07은 색상을, 08은 시간을, 09는 정렬 기준을 고르는 경우입니다. 날짜 범위를 고르는 쪽은 달력 선택 UI 9종에 있고, 사람을 나타내는 그림 없는 표시는 아바타 UI 9종에서 다룹니다.

01iOS 필 세그먼트 컨트롤

무엇이 움직이나

라디오 세 개를 균등한 폭으로 묶고, 파란 알약 배경이 선택된 칸으로 translateX 만큼 옮겨가 그 글자 위에만 얹힙니다. 이동 중간에는 알약이 scaleX(1.25)로 잡아 늘어났다가 도착 직전 1.05로 살짝 튕겨 오는 젤리 스트레치를 얹어, iOS의 스프링 감촉에 한 걸음 더 가까워집니다.

iOS 필 세그먼트 컨트롤
새 창에서 열기

01 · iOS 필 세그먼트 컨트롤 — 측정 누적면적 6.007% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateX(N*100%)균등폭 flex0.22초

코드에서 볼 곳

설정 화면의 보기 모드 전환이나 주문 상태 필터(전체/진행중/완료)에 씁니다.

.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로 옮겨가고, 고른 아이콘만 잉크색으로 짙어집니다. 필도 01과 같은 젤리 스트레치로 이동 중 scaleX 늘어남과 도착 오버슛을 거쳐, 반발력 있는 재질처럼 보입니다.

아이콘 2단 세그먼트 컨트롤
새 창에서 열기

02 · 아이콘 2단 세그먼트 컨트롤 — 측정 누적면적 4.462% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateX 50%aria-label0.18초

코드에서 볼 곳

게시판의 리스트뷰·그리드뷰 전환에 흔히 씁니다.

.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로 커집니다. 채움과 체크 각각에 오버슛 키프레임을 얹어 채워지는 폭이 한 번 넘쳤다 돌아오고 체크는 1.2배로 튕겨 올라옵니다.

다중 선택 필터 칩
새 창에서 열기

03 · 다중 선택 필터 칩 — 측정 누적면적 3.999% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scaleX 채움:has() 선택자0.2초

코드에서 볼 곳

쇼핑몰 상품 목록 필터(무료배송/당일배송/할인)에 맞습니다.

.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)로 살짝 눌렸다가 튕겨 돌아오고 테두리 링이 두꺼워집니다. 되돌아올 때 1.05를 한 번 지나치는 스프링 오버슛이 눌림의 반발력을 만들어, 손가락이 실제로 칩을 누른 듯한 감촉을 냅니다.

사이즈 라디오 칩
새 창에서 열기

04 · 사이즈 라디오 칩 — 측정 누적면적 2.096% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scale 바운스cubic-bezier 오버슈트0.3초

코드에서 볼 곳

쇼핑몰 상품 사이즈 선택이나 예약 인원수 선택에 씁니다.

.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 값이 실시간으로 바뀝니다. 떠오르는 길목에 세로로 납작했다가 가로로 살짝 넘어서는 젤리 눌림을 얹어, 말랑한 스티커가 튀어나오는 느낌을 냅니다.

이모지 리액션 팝업 피커
새 창에서 열기

05 · 이모지 리액션 팝업 피커 — 측정 누적면적 6.434% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

opacity+translateYaria-expandedEsc 로 닫힘

코드에서 볼 곳

메신저 대화나 게시글에 리액션을 남기는 자리에 씁니다.

.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 올라갑니다. 튈 때 x·y 축이 교차로 늘고 눌리는 젤리 스트레치를 얹어 커지는 중간에 좌우로 번지고 돌아올 때 세로로 반동합니다.

이모지 버스트 카운트
새 창에서 열기

06 · 이모지 버스트 카운트 — 측정 누적면적 0.487% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scale 바운스burst dot ×4aria-live

코드에서 볼 곳

쇼츠·릴스 하트 반응처럼 반응 수를 같이 보여주는 자리에 씁니다.

.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로 나타납니다. 등장할 때 1.25배로 한 번 튕기고 살짝 기울였다 돌아오는 스프링 오버슛이 붙어, 체크가 찍히는 순간을 손끝으로 확인시켜 줍니다.

그리드 컬러 스와치
새 창에서 열기

07 · 그리드 컬러 스와치 — 측정 누적면적 0.578% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scale(0)→scale(1) 체크3×2 grid0.22초

코드에서 볼 곳

상품 색상 옵션이나 테마 색상 선택 화면에 맞습니다.

.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로 밀려나가고 다음 숫자가 그 자리로 밀려 들어오며, 자동차 계기판이 넘어가듯 바뀝니다. 들어온 숫자는 자리에 앉은 뒤 세로로 1.15배 늘어났다 눌리는 젤리 안착을 한 번 거쳐, 기계식 계기판의 톱니가 물리는 반동을 흉내 냅니다.

오도미터 시간 스크럽
새 창에서 열기

08 · 오도미터 시간 스크럽 — 측정 누적면적 0.499% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateY 롤오버data-dir 버튼0.18초

코드에서 볼 곳

예약 시간이나 알람 시각을 정하는 자리에 씁니다.

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로 펼쳐지고, 고른 항목 앞에만 체크마크가 나타나며 트리거 글자도 그 옵션 이름으로 바뀝니다. 펼쳐질 때 세로로 0.9배 눌렸다 1.04배 넘어서는 젤리 오버슛을, 체크마크는 0에서 1.3배로 튕겨 오르는 스프링을 각각 얹었습니다.

체크마크 정렬 드롭다운
새 창에서 열기

09 · 체크마크 정렬 드롭다운 — 측정 누적면적 8.764% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

role=menuopacity+translateY0.2초

코드에서 볼 곳

상품 목록 정렬(최신순/가격순)이나 게시판 글 정렬에 씁니다.

.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()가 진짜 함정입니다

세그먼트 컨트롤 ui, 어디서 깨지나요?

아홉 개 중 라디오·체크박스로 만든 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+에서만 동작하고, 그 아래 버전에서는 체크 표시가 안 나타날 뿐 클릭 자체는 멀쩡히 됩니다. 알약이 로그인한 뒤의 화면에서 만나는 형태는 계정 보안 설정 UI 9종의 토글 그룹에서도 확인할 수 있습니다. 아홉 개 파일이 그대로 담긴 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

ios 세그먼트 컨트롤과 탭은 뭐가 다른가요?

탭은 눌렀을 때 보통 다른 페이지나 다른 콘텐츠 영역으로 이동하고, 세그먼트 컨트롤은 같은 화면 안에서 필터나 보기 방식처럼 값 하나만 바꿉니다. 01·02가 페이지 전환 없이 즉시 상태만 바꾸는 이유입니다. 값 하나를 고르는 바깥길 형태는 셀렉트박스 디자인 9종이 따로 다룹니다.

:has()를 못 쓰는 브라우저에서는 03·04·07이 완전히 망가지나요?

아니요. :has()가 없으면 체크·링 표시만 안 보일 뿐, 실제 <input>의 :checked 상태와 폼 제출 값은 그대로 정상 동작합니다. 시각적 피드백만 못 받을 뿐 기능 자체는 멀쩡합니다.

React판에서도 09의 메뉴가 Esc로 닫히나요?

네, react/09-sort-dropdown-checkmark-menu의 컴포넌트에도 같은 onKeyDown 리스너가 들어 있어 Esc를 누르면 open 상태가 false로 바뀌며 닫힙니다. 다만 바깥 클릭으로 닫는 처리는 함정 섹션에 적었듯 이 데모에는 빠져 있어 실제 프로젝트에 붙일 때 추가하는 걸 권합니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.