GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

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 ios segmented control clones 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. Picking a date range instead belongs to 9 Date Range Picker UI Patterns, and marking a person with no image file at all belongs to Avatar UI Design.

01iOS pill segmented control

What moves

Three radios sit in equal-width slots, and a blue pill background slides by translateX to land under whichever label is checked. Mid-flight the pill stretches to scaleX(1.25) and settles through a 1.05 overshoot — a jelly stretch that lands one step closer to the iOS spring feel.

iOS pill segmented control
Open in a new tab

01 · iOS pill segmented control — Measured area 6.007% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

In the code

Good for a view-mode switch in settings or an order-status filter (all/active/done).

.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

What moves

Choosing list or grid view slides the pill background exactly half the track width via translateX, and darkens only the picked icon. The pill takes the same jelly stretch as 01, stretching in scaleX on the way and overshooting on arrival, so the track reads as a resilient material.

Two-icon segmented toggle
Open in a new tab

02 · Two-icon segmented toggle — Measured area 4.462% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateX 50%aria-label0.18초

In the code

This is the switch behind list/grid view toggles on most boards and galleries.

.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

What moves

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. Both the fill and the check carry overshoot keyframes: the fill briefly overfills and the check springs up past 1 to 1.2 before settling.

Multi-select filter chips
Open in a new tab

03 · Multi-select filter chips — Measured area 3.999% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

In the code

Built for shop listing filters like free shipping, same-day delivery, or a discount.

.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

What moves

Picking one of the S/M/L/XL radio chips presses it down to scale(.84), then bounces back while its border ring thickens. The rebound passes through a 1.05 spring overshoot, giving the press a tangible restitution — as if a fingertip really pushed the chip.

Size radio chips
Open in a new tab

04 · Size radio chips — Measured area 2.096% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

In the code

This is the pattern behind a product size picker or a party-size selector for reservations.

.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

What moves

Pressing the react button lifts a row of four emoji into view via opacity and translateY, and the trigger's aria-expanded updates live. On the way up the bar squashes vertically then over stretches horizontally in a jelly wobble, like a soft sticker popping out.

Emoji reaction popup picker
Open in a new tab

05 · Emoji reaction popup picker — Measured area 6.434% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

opacity+translateYaria-expandedEsc 로 닫힘

In the code

This is what powers reacting to a chat message or a community post.

.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

What moves

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 bounce itself is a jelly stretch — x and y axes alternate stretching and squashing, so the emoji spreads sideways at its peak and rebounds vertically on return.

Emoji burst count badge
Open in a new tab

06 · Emoji burst count badge — Measured area 0.487% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

scale 바운스burst dot ×4aria-live

In the code

The same trick behind heart reactions on short-form video.

.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

What moves

Six swatches sit in a 3×2 grid, and picking one grows a small check badge in its corner from scale(0) to 1. The badge springs up to 1.25 with a slight tilt before settling, pinning the moment of selection to your fingertip.

Grid color swatch picker
Open in a new tab

07 · Grid color swatch picker — Measured area 0.578% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

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

In the code

Fits a grid of product color options or a theme-color picker screen.

.cw__check {
  transform: scale(0);
  transition: transform $duration $easing;
}
.cw__check-icon {          /* inline Phosphor SVG — inherits currentColor */
  position: absolute; inset: 3px;
  color: $color;
}
.cw__cell:has(.cw__input:checked) .cw__check { transform: scale(1); }

08Odometer time scrubber

What moves

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. After landing, the digit stretches to 1.15 in height then dips — a jelly settle that mimics the gear rebound of a mechanical odometer.

Odometer time scrubber
Open in a new tab

08 · Odometer time scrubber — Measured area 0.499% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateY 롤오버data-dir 버튼0.18초

In the code

Useful for picking a reservation time or setting an alarm.

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

What moves

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. The menu opens through a vertical jelly overshoot (0.9 to 1.04) and the checkmark springs in from 0 to 1.3.

Checkmark sort dropdown
Open in a new tab

09 · Checkmark sort dropdown — Measured area 8.764% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

role=menuopacity+translateY0.2초

In the code

Built for sorting a product list by newest or price, or sorting posts on a board.

.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 {               /* inline Phosphor SVG check mark */
  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

segmented control ui — where does it break?

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. Where this pill shows up on the post-login screen is the toggle group in 9 account security settings UI patterns. 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

What we checked

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.

In the code

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

Where to go next

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

Segmented control vs tabs: what is the difference?

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. The dropdown road to picking one value is what 9 Select Dropdown Design Patterns covers on its own.

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.