Component

Segmented Tabs

iOS-style segmented control with a sliding indicator that animates between segments. Used for switching time ranges and compact view modes.

  • Component
  • Tailwind
  • HTML
  • MIT

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.dev/library/tailwind/components/tabs/segmented-tabs/
fluid · no fixed width 100%
No dependencies Production-ready

Install

Add to your project

terminal
npx devsnips add Tailwind/Components/Tabs/segmented-tabs
registry path: Tailwind/Components/Tabs/segmented-tabs

Source

Implementation

code.html
<div class="w-full max-w-md">
  <div role="tablist" aria-label="View mode" class="relative inline-flex items-center gap-1 p-1 bg-gray-100 rounded-xl">
    <span class="seg-indicator absolute top-1 bottom-1 left-1 w-[calc(33.333%-0.25rem)] bg-white rounded-lg shadow-sm transition-transform duration-300 ease-out" aria-hidden="true"></span>
    <button role="tab" id="gtab-day" aria-selected="true" aria-controls="gpanel-day" tabindex="0"
      class="relative z-10 flex-1 px-4 py-1.5 text-sm font-medium text-gray-900 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 rounded-lg">
      Day
    </button>
    <button role="tab" id="gtab-week" aria-selected="false" aria-controls="gpanel-week" tabindex="-1"
      class="relative z-10 flex-1 px-4 py-1.5 text-sm font-medium text-gray-500 hover:text-gray-800 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 rounded-lg">
      Week
    </button>
    <button role="tab" id="gtab-month" aria-selected="false" aria-controls="gpanel-month" tabindex="-1"
      class="relative z-10 flex-1 px-4 py-1.5 text-sm font-medium text-gray-500 hover:text-gray-800 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 rounded-lg">
      Month
    </button>
  </div>

  <div class="mt-5">
    <div role="tabpanel" id="gpanel-day" aria-labelledby="gtab-day" tabindex="0" class="rounded-xl border border-gray-200 p-4 focus:outline-none">
      <p class="text-xs font-medium text-gray-500">Today, Aug 4</p>
      <p class="mt-1 text-lg font-semibold text-gray-900">2h 14m focused</p>
      <div class="mt-3 flex h-2 rounded-full overflow-hidden bg-gray-100">
        <div class="bg-blue-500" style="width:42%"></div><div class="bg-blue-300" style="width:28%"></div><div class="bg-gray-300" style="width:30%"></div>
      </div>
    </div>
    <div role="tabpanel" id="gpanel-week" aria-labelledby="gtab-week" tabindex="0" class="hidden rounded-xl border border-gray-200 p-4 focus:outline-none">
      <p class="text-xs font-medium text-gray-500">This week</p>
      <p class="mt-1 text-lg font-semibold text-gray-900">14h 02m focused</p>
      <div class="mt-3 flex items-end gap-1.5 h-16">
        <div class="flex-1 bg-blue-500 rounded-t" style="height:60%"></div><div class="flex-1 bg-blue-500 rounded-t" style="height:80%"></div><div class="flex-1 bg-blue-500 rounded-t" style="height:45%"></div><div class="flex-1 bg-blue-500 rounded-t" style="height:70%"></div><div class="flex-1 bg-blue-500 rounded-t" style="height:90%"></div><div class="flex-1 bg-blue-400 rounded-t" style="height:55%"></div><div class="flex-1 bg-blue-400 rounded-t" style="height:40%"></div>
      </div>
    </div>
    <div role="tabpanel" id="gpanel-month" aria-labelledby="gtab-month" tabindex="0" class="hidden rounded-xl border border-gray-200 p-4 focus:outline-none">
      <p class="text-xs font-medium text-gray-500">August</p>
      <p class="mt-1 text-lg font-semibold text-gray-900">62h 30m focused</p>
      <p class="mt-2 text-xs text-emerald-600 font-medium">▲ 18% vs last month</p>
    </div>
  </div>
</div>

<script>
  (function () {
    var list = document.querySelector('[aria-label="View mode"]');
    if (!list) return;
    var tabs = Array.prototype.slice.call(list.querySelectorAll('[role="tab"]'));
    var panels = tabs.map(function (t) { return document.getElementById(t.getAttribute('aria-controls')); });
    var indicator = list.querySelector('.seg-indicator');
    function moveIndicator(tab) {
      if (!indicator) return;
      var pad = 4;
      indicator.style.left = (tab.offsetLeft + pad) + 'px';
      indicator.style.width = (tab.offsetWidth - pad * 2) + 'px';
      indicator.style.top = pad + 'px';
      indicator.style.height = (list.offsetHeight - pad * 2) + 'px';
    }
    function activate(tab) {
      tabs.forEach(function (t) {
        var s = t === tab;
        t.setAttribute('aria-selected', s); t.tabIndex = s ? 0 : -1;
        t.classList.toggle('text-gray-900', s); t.classList.toggle('text-gray-500', !s);
      });
      moveIndicator(tab);
      panels.forEach(function (p) { if (p) p.classList.add('hidden'); });
      var panel = document.getElementById(tab.getAttribute('aria-controls'));
      if (panel) panel.classList.remove('hidden');
    }
    tabs.forEach(function (tab, i) {
      tab.addEventListener('click', function () { activate(tab); });
      tab.addEventListener('keydown', function (e) {
        var idx = null;
        if (e.key === 'ArrowRight') idx = (i + 1) % tabs.length;
        if (e.key === 'ArrowLeft') idx = (i - 1 + tabs.length) % tabs.length;
        if (idx !== null) { e.preventDefault(); tabs[idx].focus(); activate(tabs[idx]); }
      });
    });
    moveIndicator(tabs[0]);
    window.addEventListener('resize', function () {
      var active = list.querySelector('[aria-selected="true"]') || tabs[0];
      moveIndicator(active);
    });
  })();
</script>
82 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/tabs/segmented-tabs

Continue browsing

View all