GODRICH

9 Segmented Control UI Patterns — Copy-Paste

A segmented control is that pill-shaped tab switch on iOS settings screens, where picking one of a few options slides a background pill over to sit under your

This copy-paste collection covers nine selection UI patterns — segmented control, filter chips, emoji reactions, color swatches, and more — that you've probably seen in an app but never known the name of.

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine are ordered by the sequence you hit them on one screen, not by popularity. 01–02 are segmented controls for picking one of a few views, 03–04 are chips you tap to pick options, 05–06 leave an emoji reaction, 07 picks a color, 08 picks a time, and 09 picks a sort order.

01iOS pill segmented control

Three radios sit in equal-width slots, and a blue pill background slides by translateX to land under whichever label is checked. Good for a view-mode switch in settings or an order-status filter (all/active/done).

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%); }

02Two-icon segmented toggle

Choosing list or grid view slides the pill background exactly half the track width via translateX, and darkens only the picked icon. This is the switch behind list/grid view toggles on most boards and galleries.

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%); }

03Multi-select filter chips

Each chip is a checkbox; checking several at once fills their backgrounds outward from center via scaleX and pops an inline SVG check icon from scale(0) to 1. Built for shop listing filters like free shipping, same-day delivery, or a discount.

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); }

04Size radio chips

Picking one of the S/M/L/XL radio chips presses it down to scale(.84), then bounces back while its border ring thickens. This is the pattern behind a product size picker or a party-size selector for reservations.

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); }
}

05Emoji reaction popup picker

Pressing the react button lifts a row of four emoji into view via opacity and translateY, and the trigger's aria-expanded updates live. This is what powers reacting to a chat message or a community post.

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;
}

06Emoji burst count badge

Picking an emoji bounces it up in scale while four dots burst outward and fade around it, and the count badge beside it ticks up by one. The same trick behind heart reactions on short-form video.

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); }
}

07Grid color swatch picker

Six swatches sit in a 3×2 grid, and picking one grows a small check badge in its corner from scale(0) to 1. Fits a grid of product color options or a theme-color picker screen.

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); }

08Odometer time scrubber

Pressing the up/down button slides the current digit out via translateY while the next digit slides into place, like a car odometer rolling over. Useful for picking a reservation time or setting an alarm.

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');
}

09Checkmark sort dropdown

Opening the sort button reveals a list via opacity and translateY; only the chosen option shows a checkmark, and the trigger label updates to match. Built for sorting a product list by newest or price, or sorting posts on a board.

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); }

Where does this break — the hidden input and :has() trap

The radio- and checkbox-driven ones — 01, 02, 03, 04, and 07 — hide the real <input> only visually, with position: absolute; width: 1px; height: 1px; clip: rect(0,0,0,0);. Using display: none or visibility: hidden instead would have pulled the element out of the focus order entirely, so a keyboard-only visitor could never reach it with Tab. The :has() selector that 03 and 07 lean on only works on Chrome/Edge 105+, Safari 15.4+, and Firefox 121+ — on older browsers the check mark simply never appears, though clicking still works fine underneath. The zip password bundled with all nine files is e9nu36x4, and 08's odometer roll needs a 12-line roll() script because CSS alone can't swap two stacked digits and slide them at the same time. Popups you open by clicking, like 05 and 09, are also supposed to close when you click outside them or press Escape — this demo only wired up Escape to keep the code short, so add a document click listener before you ship it.

Accessibility

01, 02, 03, 04, and 07 keep the real <input> hidden but present, so screen readers announce the radio or checkbox as-is and Tab plus Space or arrow keys change the state. The popup and menu triggers in 05 and 09 update aria-expanded every time they open or close, and 09's options carry role="menuitemradio" with aria-checked to announce the current pick. 06's counter uses aria-live="polite", so a screen reader quietly picks up the new number after a reaction. Under prefers-reduced-motion: reduce, all nine drop their transitions and animations but keep whatever state they landed in — a checked pill, a filled chip, an open menu — so no status information disappears with the motion.

@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; }
}

More real-world UI patterns like these live in the UI category, and how this site tests every demo before publishing is on the about page.

FAQ

What's the difference between a segmented control and tabs?

Tabs usually navigate to a different page or content area when clicked; a segmented control just changes one value, like a filter or view mode, within the same screen. That's why 01 and 02 update instantly with no page change at all.

Do 03, 04, and 07 completely break in browsers without :has()?

No. Without :has() the check mark or ring simply doesn't render — the underlying <input>'s :checked state and form submission value keep working normally. You lose the visual confirmation, not the function.

Does the React version of 09 also close on Escape?

Yes — the component in react/09-sort-dropdown-checkmark-menu wires up the same onKeyDown listener, so pressing Escape flips open back to false and closes the menu. Closing on an outside click is left out here too, as the trap section explains, so add it before shipping to production.

Enter the archive password

The password is inside this article. You will find it as you read.