32 CSS Tab Designs06 / 32

CSS + JSMIT licensed

iOS Segmented

Apple-style segmented control: gray track, white pill that physically slides between segments. JS measures positions live; resize-listener keeps the pill aligned at any width.

Published

Live Demo
Try it

The code

<div class="tt26">
  <nav class="tt26n">
    <span class="tt26pill" aria-hidden="true"></span>
    <button class="tt26b active" data-t>Day</button>
    <button class="tt26b" data-t>Week</button>
    <button class="tt26b" data-t>Month</button>
  </nav>
</div>
.tt26 {
  background: #f2f2f7;
  padding: 32px 24px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  min-height: 100vh;
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tt26n {
  position: relative;
  display: flex;
  background: rgba(120, 120, 128, 0.16);
  border-radius: 9px;
  padding: 2px;
  width: 100%;
  max-width: 380px;
  min-width: 0;
}

.tt26pill {
  position: absolute;
  top: 2px;
  left: 2px;
  height: calc(100% - 4px);
  background: #fff;
  border-radius: 7px;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.06);
  transition: left 0.34s cubic-bezier(0.4, 0, 0.2, 1), width 0.34s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
}

.tt26b {
  flex: 1;
  min-width: 0;
  padding: 8px 14px;
  border: 0;
  background: transparent;
  font: 600 13px/1 inherit;
  color: #1c1c1e;
  cursor: pointer;
  position: relative;
  z-index: 1;
  transition: color 0.2s;
  white-space: nowrap;
}

.tt26b:focus-visible {
  outline: 2px solid #007aff;
  outline-offset: 2px;
  border-radius: 7px;
}

@media (prefers-reduced-motion: reduce) {
  .tt26pill {
    transition: none;
  }
}
/* iOS Segmented — toggle .active and slide white pill between tabs.
   Re-positions on viewport resize so the pill stays locked to its segment. */
(function () {
  var nav = document.querySelector(".tt26n");
  if (!nav) return;
  var btns = nav.querySelectorAll("[data-t]");
  var pill = nav.querySelector(".tt26pill");
  var current = null;

  function reposition() {
    if (!current || !pill) return;
    pill.style.left = current.offsetLeft + "px";
    pill.style.width = current.offsetWidth + "px";
  }
  function activate(btn) {
    current = btn;
    btns.forEach(function (b) {
      b.classList.toggle("active", b === btn);
    });
    reposition();
  }
  btns.forEach(function (b) {
    b.addEventListener("click", function () {
      activate(b);
    });
  });
  window.addEventListener("resize", reposition);
  var initial = nav.querySelector("[data-t].active") || btns[0];
  if (initial) activate(initial);
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Tab Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: iOS Segmented
Source: https://codefronts.com/navigation/css-tabs/ios-segmented/

Apple-style segmented control: gray track, white pill that physically slides between segments. JS measures positions live; resize-listener keeps the pill aligned at any width.
## HTML
```html
<div class="tt26">
  <nav class="tt26n">
    <span class="tt26pill" aria-hidden="true"></span>
    <button class="tt26b active" data-t>Day</button>
    <button class="tt26b" data-t>Week</button>
    <button class="tt26b" data-t>Month</button>
  </nav>
</div>
```
## CSS
```css
.tt26 {
  background: #f2f2f7;
  padding: 32px 24px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  min-height: 100vh;
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tt26n {
  position: relative;
  display: flex;
  background: rgba(120, 120, 128, 0.16);
  border-radius: 9px;
  padding: 2px;
  width: 100%;
  max-width: 380px;
  min-width: 0;
}

.tt26pill {
  position: absolute;
  top: 2px;
  left: 2px;
  height: calc(100% - 4px);
  background: #fff;
  border-radius: 7px;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.06);
  transition: left 0.34s cubic-bezier(0.4, 0, 0.2, 1), width 0.34s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
}

.tt26b {
  flex: 1;
  min-width: 0;
  padding: 8px 14px;
  border: 0;
  background: transparent;
  font: 600 13px/1 inherit;
  color: #1c1c1e;
  cursor: pointer;
  position: relative;
  z-index: 1;
  transition: color 0.2s;
  white-space: nowrap;
}

.tt26b:focus-visible {
  outline: 2px solid #007aff;
  outline-offset: 2px;
  border-radius: 7px;
}

@media (prefers-reduced-motion: reduce) {
  .tt26pill {
    transition: none;
  }
}
```

## JavaScript
```js
/* iOS Segmented — toggle .active and slide white pill between tabs.
   Re-positions on viewport resize so the pill stays locked to its segment. */
(function () {
  var nav = document.querySelector(".tt26n");
  if (!nav) return;
  var btns = nav.querySelectorAll("[data-t]");
  var pill = nav.querySelector(".tt26pill");
  var current = null;

  function reposition() {
    if (!current || !pill) return;
    pill.style.left = current.offsetLeft + "px";
    pill.style.width = current.offsetWidth + "px";
  }
  function activate(btn) {
    current = btn;
    btns.forEach(function (b) {
      b.classList.toggle("active", b === btn);
    });
    reposition();
  }
  btns.forEach(function (b) {
    b.addEventListener("click", function () {
      activate(b);
    });
  });
  window.addEventListener("resize", reposition);
  var initial = nav.querySelector("[data-t].active") || btns[0];
  if (initial) activate(initial);
})();
```

How this works

The .tt26 demo recreates the iOS segmented control from the Apple Human Interface Guidelines: a rounded gray track at rgba(120, 120, 128, 0.16) holding three flex-sized buttons, with a white .tt26pill sliding underneath the active option. The pill is absolutely positioned, sits at top: 2px; left: 2px, and takes its width and left offset from JavaScript that measures the active button's offsetLeft and offsetWidth on every activation and window resize.

Each .tt26b button carries flex: 1 so the three segments share the 380px maximum width equally, and z-index: 1 lifts the label text above the pill layer. On click the handler toggles the .active class, then calls reposition(), which writes inline left and width values to the pill. Both properties are transitioned with cubic-bezier(0.4, 0, 0.2, 1) over 0.34s, giving the physical glide Apple uses in Settings and Calendar. Label color fades from #1c1c1e uniformly across states because the pill, not the text, carries the active signal.

The whole block is hand-coded, framework-agnostic, and copy-paste ready with no dependencies. The SF-first font stack starts with -apple-system and BlinkMacSystemFont, falling back to Segoe UI and system-ui, so the type register matches the platform on every OS. Focus rings use the iOS accent #007aff at 2px with a 2px offset, and the prefers-reduced-motion query strips the pill transition to zero so vestibular users get instant snaps instead of glide. SSR-safe because the JS only touches the DOM after mount and the pill starts invisible until the first activate() call sets its geometry.

Make it yours

  • Swap the track color rgba(120, 120, 128, 0.16) and pill background #fff for a dark-mode pair like rgba(120, 120, 128, 0.32) and #636366 to match iOS 17 dark segmented controls.
  • Change border-radius: 9px on the track and 7px on the pill together; the 2px gap between them is what sells the inset look, so keep the difference at exactly 2 to preserve it.
  • Add a fourth or fifth segment without touching CSS: each button uses flex: 1, so widths auto-balance and the JS measures live offsets so the pill lands correctly on any count.
  • Tune the glide by editing the 0.34s cubic-bezier(0.4, 0, 0.2, 1) transition on .tt26pill; shorter durations feel snappier, longer ones read as luxurious but delay perceived response.
  • Retint the focus ring by replacing #007aff with your brand accent, and match the pill shadow tokens 0 3px 8px rgba(0,0,0,0.12) to sit on any track color without a hard edge.

Gotchas — read before shipping

  • The pill uses inline left and width, so if you server-render the markup the pill sits at 0, auto until the JS runs; either hydrate immediately or set an initial inline style matching the first segment to avoid a flash of misaligned indicator.
  • For a real WAI-ARIA tabs pattern, add role="tablist" to the nav, role="tab" and aria-selected to each button, and wire arrow-key navigation; the demo ships as a plain button group and needs those attributes before it becomes an accessible tab widget.
  • The resize listener is not throttled; on rapid drags in split-view browsers it will fire dozens of times per second. Wrap the callback in requestAnimationFrame or a 100ms debounce to stay INP-safe on lower-end devices.
  • If the container is display: none on load (in a hidden dialog or off-screen carousel), offsetLeft and offsetWidth return 0 and the pill sizes to nothing; re-run reposition() when the parent becomes visible using an IntersectionObserver or a manual call after your reveal transition.
  • Font-loading shifts widths: if a webfont swaps in after activation the pill will misalign until the next click. Preload the display font, use font-display: optional, or listen for document.fonts.ready and call reposition() once resolved.
  • React and Vue rerenders can replace the pill node between activations, wiping the inline styles set by reposition(); move the sliding-indicator logic into a useLayoutEffect hook that runs after commit, or read positions from refs each render instead of storing them in state.

Browser support

ChromeSafariFirefoxEdge
51+10+52+51+

Flex, cubic-bezier, focus-visible, and prefers-reduced-motion are all baseline-safe. focus-visible landed in Safari 15.4 and Firefox 85; earlier versions fall back to a permanent outline on click, which is acceptable but louder than intended.

Techniques used in this demo

Search CodeFronts

Loading…