Tailwind Tabs: 16 Accessible Examples, From Pills to Vertical

Tailwind CSS has no tabs component. Tailwind tabs are utility classes arranged so several panels share one view and only one shows at a time, while JavaScript, Alpine.js, Headless UI or CSS-only radio inputs do the switching.

Below are 16 Tailwind tabs examples you can try and copy, followed by a guide to markup, accessibility, styling and responsive layouts.

By Bogdan Sandu ยท Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Accessible tabs (W3C pattern)

Four tab sets that follow the W3C APG tabs pattern with role="tablist", aria-selected, aria-controls and a roving tabindex. They show automatic and manual activation, vertical orientation and a select fallback for phones.

Underline tabs with a sliding indicator

  • aria-selected:
  • motion-safe:
  • scrollbar-none
  • duration-300

Click a tab, or focus one and use the arrow keys, Home and End.

The tabs follow the W3C APG pattern with automatic activation: arrow keys, Home and End move focus and show the panel at once, and only the selected tab sits in the Tab order (roving tabindex). aria-selected:text-teal-700 colors the active label, so the script only flips attributes. One absolutely positioned span is the indicator; the script sets its width and translateX from the selected tab, and motion-safe:transition-[transform,width] makes it slide only for people who have not asked for reduced motion.

View code
<div class="ttt-tabs w-full max-w-2xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
    <h3 class="text-lg font-semibold text-slate-900">Harbor Lane redesign</h3>
    <p class="text-sm text-slate-500">Updated 2 hours ago</p>
  </div>
  <div role="tablist" aria-label="Project sections" class="relative mt-4 flex gap-6 overflow-x-auto border-b border-slate-200 scrollbar-none">
    <button type="button" role="tab" id="ttt-ul-tab-1" aria-controls="ttt-ul-panel-1" aria-selected="true" tabindex="0" class="shrink-0 cursor-pointer rounded-sm py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:text-slate-800 aria-selected:text-teal-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-teal-600">Overview</button>
    <button type="button" role="tab" id="ttt-ul-tab-2" aria-controls="ttt-ul-panel-2" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-sm py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:text-slate-800 aria-selected:text-teal-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-teal-600">Activity</button>
    <button type="button" role="tab" id="ttt-ul-tab-3" aria-controls="ttt-ul-panel-3" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-sm py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:text-slate-800 aria-selected:text-teal-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-teal-600">Files</button>
    <button type="button" role="tab" id="ttt-ul-tab-4" aria-controls="ttt-ul-panel-4" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-sm py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:text-slate-800 aria-selected:text-teal-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-teal-600">Members</button>
    <span class="ttt-bar pointer-events-none absolute bottom-0 left-0 h-0.5 w-0 rounded-full bg-teal-600 duration-300 ease-out" aria-hidden="true"></span>
  </div>
  <div role="tabpanel" id="ttt-ul-panel-1" aria-labelledby="ttt-ul-tab-1" tabindex="0" class="rounded-lg pt-5 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-teal-600">
    <p class="text-[15px] leading-relaxed text-slate-600">A new booking flow and brand refresh for a harbor-side restaurant group. Design is signed off; development started this week.</p>
    <dl class="mt-5 grid grid-cols-1 gap-3 sm:grid-cols-3">
      <div class="rounded-xl bg-slate-50 p-4"><dt class="text-xs font-medium text-slate-500">Tasks done</dt><dd class="mt-1 text-xl font-semibold text-slate-900">24 of 31</dd></div>
      <div class="rounded-xl bg-slate-50 p-4"><dt class="text-xs font-medium text-slate-500">Next milestone</dt><dd class="mt-1 text-xl font-semibold text-slate-900">Nov 14</dd></div>
      <div class="rounded-xl bg-slate-50 p-4"><dt class="text-xs font-medium text-slate-500">Budget used</dt><dd class="mt-1 text-xl font-semibold text-slate-900">68%</dd></div>
    </dl>
  </div>
  <div role="tabpanel" id="ttt-ul-panel-2" aria-labelledby="ttt-ul-tab-2" tabindex="0" hidden class="rounded-lg pt-5 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-teal-600">
    <ol class="space-y-4 text-[15px]">
      <li class="flex gap-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-teal-700 text-xs font-semibold text-white" aria-hidden="true">MQ</span><p class="text-slate-600"><span class="font-medium text-slate-900">Mara Quinn</span> moved <span class="font-medium text-slate-900">Table picker</span> to review <span class="block text-sm text-slate-500">2 hours ago</span></p></li>
      <li class="flex gap-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-amber-600 text-xs font-semibold text-white" aria-hidden="true">DP</span><p class="text-slate-600"><span class="font-medium text-slate-900">Dev Patel</span> commented on <span class="font-medium text-slate-900">Menu page</span> <span class="block text-sm text-slate-500">Yesterday</span></p></li>
      <li class="flex gap-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-slate-700 text-xs font-semibold text-white" aria-hidden="true">LO</span><p class="text-slate-600"><span class="font-medium text-slate-900">Lena Ortiz</span> uploaded 12 photos <span class="block text-sm text-slate-500">Monday</span></p></li>
    </ol>
  </div>
  <div role="tabpanel" id="ttt-ul-panel-3" aria-labelledby="ttt-ul-tab-3" tabindex="0" hidden class="rounded-lg pt-5 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-teal-600">
    <ul class="divide-y divide-slate-100 text-[15px]">
      <li class="flex items-center justify-between gap-4 py-3"><span class="min-w-0 truncate font-medium text-slate-900">booking-flow-v3.fig</span><span class="shrink-0 text-sm text-slate-500">18.4 MB</span></li>
      <li class="flex items-center justify-between gap-4 py-3"><span class="min-w-0 truncate font-medium text-slate-900">brand-guidelines.pdf</span><span class="shrink-0 text-sm text-slate-500">6.1 MB</span></li>
      <li class="flex items-center justify-between gap-4 py-3"><span class="min-w-0 truncate font-medium text-slate-900">menu-photography.zip</span><span class="shrink-0 text-sm text-slate-500">212 MB</span></li>
    </ul>
  </div>
  <div role="tabpanel" id="ttt-ul-panel-4" aria-labelledby="ttt-ul-tab-4" tabindex="0" hidden class="rounded-lg pt-5 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-teal-600">
    <ul class="grid gap-3 sm:grid-cols-2">
      <li class="flex items-center gap-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-teal-700 text-xs font-semibold text-white" aria-hidden="true">MQ</span><span class="text-[15px]"><span class="block font-medium text-slate-900">Mara Quinn</span><span class="text-sm text-slate-500">Product designer</span></span></li>
      <li class="flex items-center gap-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-amber-600 text-xs font-semibold text-white" aria-hidden="true">DP</span><span class="text-[15px]"><span class="block font-medium text-slate-900">Dev Patel</span><span class="text-sm text-slate-500">Front-end developer</span></span></li>
      <li class="flex items-center gap-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-slate-700 text-xs font-semibold text-white" aria-hidden="true">LO</span><span class="text-[15px]"><span class="block font-medium text-slate-900">Lena Ortiz</span><span class="text-sm text-slate-500">Photographer</span></span></li>
      <li class="flex items-center gap-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-rose-700 text-xs font-semibold text-white" aria-hidden="true">OB</span><span class="text-[15px]"><span class="block font-medium text-slate-900">Owen Brandt</span><span class="text-sm text-slate-500">Client lead</span></span></li>
    </ul>
  </div>
</div>

Folder tabs with manual activation

  • aria-selected:border-b-white
  • -mb-px
  • rounded-t-lg
  • data-manual

Focus a tab and press the arrow keys, then Enter or Space to open it.

Folder tabs overlap the panel by one pixel: every tab has -mb-px and a transparent border, and the selected one turns its border visible with a white bottom edge, so it merges into the panel below. This set uses manual activation, the W3C option for panels that are slow to render: arrow keys only move focus, and Enter or Space (a native button click) shows the panel. The data-manual attribute on the tablist switches the shared script into that mode.

View code
<div class="ttt-tabs w-full max-w-xl">
  <h3 id="ttt-fd-label" class="mb-3 text-base font-semibold text-slate-900">Q3 2026 report, Northwind Bikes</h3>
  <div role="tablist" aria-labelledby="ttt-fd-label" data-manual class="flex gap-1 border-b border-slate-300">
    <button type="button" role="tab" id="ttt-fd-tab-1" aria-controls="ttt-fd-panel-1" aria-selected="true" tabindex="0" class="relative -mb-px cursor-pointer rounded-t-lg border border-transparent px-2.5 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-200/60 hover:text-slate-900 aria-selected:border-slate-300 aria-selected:border-b-white aria-selected:bg-white aria-selected:text-blue-800 aria-selected:hover:bg-white sm:px-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">Summary</button>
    <button type="button" role="tab" id="ttt-fd-tab-2" aria-controls="ttt-fd-panel-2" aria-selected="false" tabindex="-1" class="relative -mb-px cursor-pointer rounded-t-lg border border-transparent px-2.5 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-200/60 hover:text-slate-900 aria-selected:border-slate-300 aria-selected:border-b-white aria-selected:bg-white aria-selected:text-blue-800 aria-selected:hover:bg-white sm:px-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">Revenue</button>
    <button type="button" role="tab" id="ttt-fd-tab-3" aria-controls="ttt-fd-panel-3" aria-selected="false" tabindex="-1" class="relative -mb-px cursor-pointer rounded-t-lg border border-transparent px-2.5 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-200/60 hover:text-slate-900 aria-selected:border-slate-300 aria-selected:border-b-white aria-selected:bg-white aria-selected:text-blue-800 aria-selected:hover:bg-white sm:px-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">Costs</button>
    <button type="button" role="tab" id="ttt-fd-tab-4" aria-controls="ttt-fd-panel-4" aria-selected="false" tabindex="-1" class="relative -mb-px cursor-pointer rounded-t-lg border border-transparent px-2.5 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-200/60 hover:text-slate-900 aria-selected:border-slate-300 aria-selected:border-b-white aria-selected:bg-white aria-selected:text-blue-800 aria-selected:hover:bg-white sm:px-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">Outlook</button>
  </div>
  <div role="tabpanel" id="ttt-fd-panel-1" aria-labelledby="ttt-fd-tab-1" tabindex="0" class="rounded-b-xl border border-t-0 border-slate-300 bg-white p-5 sm:p-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">
    <p class="text-sm leading-relaxed text-slate-600">Strongest quarter since launch, driven by the cargo bike line and a lower return rate.</p>
    <dl class="mt-4">
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Revenue</dt><dd class="font-semibold tabular-nums text-slate-900">$4.82M</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Gross margin</dt><dd class="font-semibold tabular-nums text-slate-900">41.3%</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Orders</dt><dd class="font-semibold tabular-nums text-slate-900">9,614</dd></div>
    </dl>
  </div>
  <div role="tabpanel" id="ttt-fd-panel-2" aria-labelledby="ttt-fd-tab-2" tabindex="0" hidden class="rounded-b-xl border border-t-0 border-slate-300 bg-white p-5 sm:p-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">
    <dl>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Cargo bikes</dt><dd class="font-semibold tabular-nums text-slate-900">$2.10M</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">City bikes</dt><dd class="font-semibold tabular-nums text-slate-900">$1.64M</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Parts and service</dt><dd class="font-semibold tabular-nums text-slate-900">$1.08M</dd></div>
    </dl>
  </div>
  <div role="tabpanel" id="ttt-fd-panel-3" aria-labelledby="ttt-fd-tab-3" tabindex="0" hidden class="rounded-b-xl border border-t-0 border-slate-300 bg-white p-5 sm:p-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">
    <dl>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Components</dt><dd class="font-semibold tabular-nums text-slate-900">$1.92M</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Logistics</dt><dd class="font-semibold tabular-nums text-slate-900">$0.61M</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Marketing</dt><dd class="font-semibold tabular-nums text-slate-900">$0.31M</dd></div>
    </dl>
  </div>
  <div role="tabpanel" id="ttt-fd-panel-4" aria-labelledby="ttt-fd-tab-4" tabindex="0" hidden class="rounded-b-xl border border-t-0 border-slate-300 bg-white p-5 sm:p-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700">
    <p class="text-sm leading-relaxed text-slate-600">Q4 guidance assumes a flat holiday season and the new Lisbon warehouse opening on December 1.</p>
    <dl class="mt-4">
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Q4 revenue target</dt><dd class="font-semibold tabular-nums text-slate-900">$5.10M</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Hiring plan</dt><dd class="font-semibold tabular-nums text-slate-900">6 roles</dd></div>
      <div class="flex items-baseline justify-between gap-4 border-b border-slate-100 py-2.5 last:border-0"><dt class="text-sm text-slate-600">Capex</dt><dd class="font-semibold tabular-nums text-slate-900">$0.45M</dd></div>
    </dl>
  </div>
</div>

Vertical tabs that turn into a scroller on phones

  • md:flex-col
  • overflow-x-auto
  • aria-orientation
  • matchMedia

Use Up and Down on desktop, Left and Right on a phone.

One tablist, two layouts: below 768px it is a horizontally scrolling row (flex overflow-x-auto scrollbar-none), from md: up it becomes a left column with md:flex-col md:w-48 and a md:border-l-2 marker. Because the W3C pattern ties aria-orientation and the arrow keys to the visual layout, a matchMedia listener sets aria-orientation to vertical or horizontal, and the shared script reads it to choose Up/Down or Left/Right. On the scroller, the selected tab is kept in view.

View code
<div class="ttt-tabs w-full max-w-3xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <h3 id="ttt-vt-label" class="text-lg font-semibold text-slate-900">Help center</h3>
  <div class="mt-4 flex flex-col gap-5 md:flex-row md:gap-8">
    <div role="tablist" aria-labelledby="ttt-vt-label" aria-orientation="horizontal" class="relative -mx-1 flex gap-1 overflow-x-auto px-1 py-1 scrollbar-none md:mx-0 md:w-48 md:shrink-0 md:flex-col md:overflow-visible md:px-0">
    <button type="button" role="tab" id="ttt-vt-tab-1" aria-controls="ttt-vt-panel-1" aria-selected="true" tabindex="0" class="shrink-0 cursor-pointer rounded-lg px-3.5 py-2.5 text-left text-sm font-medium whitespace-nowrap text-slate-600 hover:bg-slate-100 hover:text-slate-900 aria-selected:bg-sky-50 aria-selected:text-sky-800 md:rounded-l-none md:border-l-2 md:border-transparent md:aria-selected:border-sky-600 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600">Getting started</button>
    <button type="button" role="tab" id="ttt-vt-tab-2" aria-controls="ttt-vt-panel-2" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-lg px-3.5 py-2.5 text-left text-sm font-medium whitespace-nowrap text-slate-600 hover:bg-slate-100 hover:text-slate-900 aria-selected:bg-sky-50 aria-selected:text-sky-800 md:rounded-l-none md:border-l-2 md:border-transparent md:aria-selected:border-sky-600 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600">Billing</button>
    <button type="button" role="tab" id="ttt-vt-tab-3" aria-controls="ttt-vt-panel-3" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-lg px-3.5 py-2.5 text-left text-sm font-medium whitespace-nowrap text-slate-600 hover:bg-slate-100 hover:text-slate-900 aria-selected:bg-sky-50 aria-selected:text-sky-800 md:rounded-l-none md:border-l-2 md:border-transparent md:aria-selected:border-sky-600 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600">Integrations</button>
    <button type="button" role="tab" id="ttt-vt-tab-4" aria-controls="ttt-vt-panel-4" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-lg px-3.5 py-2.5 text-left text-sm font-medium whitespace-nowrap text-slate-600 hover:bg-slate-100 hover:text-slate-900 aria-selected:bg-sky-50 aria-selected:text-sky-800 md:rounded-l-none md:border-l-2 md:border-transparent md:aria-selected:border-sky-600 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600">Security</button>
    <button type="button" role="tab" id="ttt-vt-tab-5" aria-controls="ttt-vt-panel-5" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-lg px-3.5 py-2.5 text-left text-sm font-medium whitespace-nowrap text-slate-600 hover:bg-slate-100 hover:text-slate-900 aria-selected:bg-sky-50 aria-selected:text-sky-800 md:rounded-l-none md:border-l-2 md:border-transparent md:aria-selected:border-sky-600 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600">Your account</button>
    </div>
    <div role="tabpanel" id="ttt-vt-panel-1" aria-labelledby="ttt-vt-tab-1" class="min-w-0 flex-1 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600">
    <h4 class="text-base font-semibold text-slate-900">Getting started</h4>
    <p class="mt-1 text-sm text-slate-500">Set up your workspace in about ten minutes.</p>
    <ul class="mt-3 -mx-3">
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Create your first workspace<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Invite your team<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Import data from a spreadsheet<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
    </ul>
    </div>
    <div role="tabpanel" id="ttt-vt-panel-2" aria-labelledby="ttt-vt-tab-2" hidden class="min-w-0 flex-1 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600">
    <h4 class="text-base font-semibold text-slate-900">Billing</h4>
    <p class="mt-1 text-sm text-slate-500">Plans, invoices and payment methods.</p>
    <ul class="mt-3 -mx-3">
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Change your plan<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Download past invoices<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Update the billing email<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
    </ul>
    </div>
    <div role="tabpanel" id="ttt-vt-panel-3" aria-labelledby="ttt-vt-tab-3" hidden class="min-w-0 flex-1 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600">
    <h4 class="text-base font-semibold text-slate-900">Integrations</h4>
    <p class="mt-1 text-sm text-slate-500">Connect the tools your team already uses.</p>
    <ul class="mt-3 -mx-3">
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Connect a calendar<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Send alerts to a chat channel<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Use the REST API<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
    </ul>
    </div>
    <div role="tabpanel" id="ttt-vt-panel-4" aria-labelledby="ttt-vt-tab-4" hidden class="min-w-0 flex-1 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600">
    <h4 class="text-base font-semibold text-slate-900">Security</h4>
    <p class="mt-1 text-sm text-slate-500">Keep accounts and data protected.</p>
    <ul class="mt-3 -mx-3">
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Turn on two-step sign-in<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Review active sessions<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Set up single sign-on<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
    </ul>
    </div>
    <div role="tabpanel" id="ttt-vt-panel-5" aria-labelledby="ttt-vt-tab-5" hidden class="min-w-0 flex-1 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600">
    <h4 class="text-base font-semibold text-slate-900">Your account</h4>
    <p class="mt-1 text-sm text-slate-500">Profile, notifications and data export.</p>
    <ul class="mt-3 -mx-3">
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Change your display name<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Choose which emails you get<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
      <li><a href="#" class="group flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-[15px] text-slate-700 hover:bg-slate-50 hover:text-sky-800 focus-visible:outline-2 focus-visible:outline-sky-600">Export or delete your data<svg class="size-4 shrink-0 text-slate-400 group-hover:text-sky-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></a></li>
    </ul>
    </div>
  </div>
</div>

Tabs with a select fallback on phones

  • sm:hidden
  • hidden sm:flex
  • appearance-none
  • aria-selected:border-rose-600

On a phone, pick a status from the menu. On desktop, use the tabs.

Below the sm breakpoint (40rem) a native select replaces the tab row: the select takes sm:hidden and the tablist hidden sm:flex, so only one control is ever visible. Both drive the same select function, and every switch writes the tab id back into select.value, so the two stay in sync when the screen crosses the breakpoint. appearance-none plus an absolutely positioned chevron gives the select a consistent look without the forms plugin.

View code
<div class="ttt-tabs w-full max-w-2xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <h3 id="ttt-or-label" class="text-lg font-semibold text-slate-900">Orders</h3>
  <div class="relative mt-4 sm:hidden">
    <label for="ttt-or-pick" class="sr-only">Show orders</label>
    <select id="ttt-or-pick" class="ttt-picker w-full cursor-pointer appearance-none rounded-lg border border-slate-300 bg-white py-2.5 pr-10 pl-3 text-[15px] font-medium text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-600">
      <option value="ttt-or-tab-1">All orders (128)</option>
      <option value="ttt-or-tab-2">Processing (6)</option>
      <option value="ttt-or-tab-3">Shipped (14)</option>
      <option value="ttt-or-tab-4">Returns (2)</option>
    </select>
    <svg class="pointer-events-none absolute top-1/2 right-3 size-5 -translate-y-1/2 text-slate-500" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m6 8 4 4 4-4" stroke-linecap="round" stroke-linejoin="round"/></svg>
  </div>
  <div role="tablist" aria-labelledby="ttt-or-label" class="mt-3 hidden gap-6 border-b border-slate-200 sm:flex">
    <button type="button" role="tab" id="ttt-or-tab-1" aria-controls="ttt-or-panel-1" aria-selected="true" tabindex="0" class="-mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-rose-600 aria-selected:text-rose-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600">All orders <span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">128</span></button>
    <button type="button" role="tab" id="ttt-or-tab-2" aria-controls="ttt-or-panel-2" aria-selected="false" tabindex="-1" class="-mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-rose-600 aria-selected:text-rose-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600">Processing <span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">6</span></button>
    <button type="button" role="tab" id="ttt-or-tab-3" aria-controls="ttt-or-panel-3" aria-selected="false" tabindex="-1" class="-mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-rose-600 aria-selected:text-rose-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600">Shipped <span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">14</span></button>
    <button type="button" role="tab" id="ttt-or-tab-4" aria-controls="ttt-or-panel-4" aria-selected="false" tabindex="-1" class="-mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-rose-600 aria-selected:text-rose-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600">Returns <span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">2</span></button>
  </div>
  <div class="mt-2">
  <div role="tabpanel" id="ttt-or-panel-1" aria-labelledby="ttt-or-tab-1" tabindex="0" class="rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-rose-600">
    <ul class="divide-y divide-slate-100">
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20418, Ines Varga</span><span class="text-sm text-slate-500">Today, 9:42</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$184.00</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-amber-50 text-amber-800 ring-amber-200">Processing</span></span></li>
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20417, Tom Akers</span><span class="text-sm text-slate-500">Today, 8:15</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$62.50</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-sky-50 text-sky-800 ring-sky-200">Shipped</span></span></li>
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20412, Aya Mori</span><span class="text-sm text-slate-500">Yesterday</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$310.20</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-emerald-50 text-emerald-800 ring-emerald-200">Delivered</span></span></li>
    </ul>
  </div>
  <div role="tabpanel" id="ttt-or-panel-2" aria-labelledby="ttt-or-tab-2" tabindex="0" hidden class="rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-rose-600">
    <ul class="divide-y divide-slate-100">
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20418, Ines Varga</span><span class="text-sm text-slate-500">Today, 9:42</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$184.00</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-amber-50 text-amber-800 ring-amber-200">Processing</span></span></li>
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20416, Luis Pardo</span><span class="text-sm text-slate-500">Today, 7:58</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$45.90</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-amber-50 text-amber-800 ring-amber-200">Processing</span></span></li>
    </ul>
  </div>
  <div role="tabpanel" id="ttt-or-panel-3" aria-labelledby="ttt-or-tab-3" tabindex="0" hidden class="rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-rose-600">
    <ul class="divide-y divide-slate-100">
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20417, Tom Akers</span><span class="text-sm text-slate-500">Today, 8:15</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$62.50</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-sky-50 text-sky-800 ring-sky-200">Shipped</span></span></li>
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20409, Nora Wells</span><span class="text-sm text-slate-500">Oct 27</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$129.00</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-sky-50 text-sky-800 ring-sky-200">Shipped</span></span></li>
    </ul>
  </div>
  <div role="tabpanel" id="ttt-or-panel-4" aria-labelledby="ttt-or-tab-4" tabindex="0" hidden class="rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-rose-600">
    <ul class="divide-y divide-slate-100">
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20377, Sam Okafor</span><span class="text-sm text-slate-500">Oct 22</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$89.00</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-slate-100 text-slate-700 ring-slate-200">Returned</span></span></li>
      <li class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-3"><span class="min-w-0"><span class="block text-[15px] font-medium text-slate-900">#20351, Jade Lin</span><span class="text-sm text-slate-500">Oct 19</span></span><span class="flex items-center gap-3"><span class="text-sm font-semibold tabular-nums text-slate-900">$24.99</span><span class="rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset bg-slate-100 text-slate-700 ring-slate-200">Returned</span></span></li>
    </ul>
  </div>
  </div>
</div>

Pill, icon, scrolling and dark tabs

The same markup and script under five different looks: a segmented control, tabs with icons and badges, a scrolling row, full-width steps with a disabled tab, and a dark theme.

Segmented control tabs over a chart

  • aria-selected:bg-white
  • aria-selected:shadow-xs
  • h-[%]
  • tabular-nums

Pick a period.

A segmented control is a tablist with a tinted track: bg-slate-100 p-1 on the list, and aria-selected:bg-white aria-selected:shadow-xs lift the active segment. The bars are flex children with arbitrary heights such as h-[72%], hidden from screen readers with aria-hidden, while an sr-only sentence states what the chart shows. tabular-nums keeps the figures from shifting as the numbers change.

View code
<div class="ttt-tabs w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <h3 id="ttt-sg-label" class="text-base font-semibold text-slate-900">Store revenue</h3>
    <div role="tablist" aria-labelledby="ttt-sg-label" class="inline-flex rounded-xl bg-slate-100 p-1">
    <button type="button" role="tab" id="ttt-sg-tab-1" aria-controls="ttt-sg-panel-1" aria-selected="true" tabindex="0" class="cursor-pointer rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 aria-selected:bg-white aria-selected:text-slate-900 aria-selected:shadow-xs motion-safe:transition-colors sm:px-4 focus-visible:outline-2 focus-visible:outline-emerald-700">Day</button>
    <button type="button" role="tab" id="ttt-sg-tab-2" aria-controls="ttt-sg-panel-2" aria-selected="false" tabindex="-1" class="cursor-pointer rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 aria-selected:bg-white aria-selected:text-slate-900 aria-selected:shadow-xs motion-safe:transition-colors sm:px-4 focus-visible:outline-2 focus-visible:outline-emerald-700">Week</button>
    <button type="button" role="tab" id="ttt-sg-tab-3" aria-controls="ttt-sg-panel-3" aria-selected="false" tabindex="-1" class="cursor-pointer rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 aria-selected:bg-white aria-selected:text-slate-900 aria-selected:shadow-xs motion-safe:transition-colors sm:px-4 focus-visible:outline-2 focus-visible:outline-emerald-700">Month</button>
    <button type="button" role="tab" id="ttt-sg-tab-4" aria-controls="ttt-sg-panel-4" aria-selected="false" tabindex="-1" class="cursor-pointer rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 aria-selected:bg-white aria-selected:text-slate-900 aria-selected:shadow-xs motion-safe:transition-colors sm:px-4 focus-visible:outline-2 focus-visible:outline-emerald-700">Year</button>
    </div>
  </div>
  <div role="tabpanel" id="ttt-sg-panel-1" aria-labelledby="ttt-sg-tab-1" tabindex="0" class="mt-5 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-700">
    <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
      <p class="text-3xl font-semibold tracking-tight text-slate-900 tabular-nums">$1,284</p>
      <p class="rounded-full px-2 py-0.5 text-xs font-semibold bg-emerald-50 text-emerald-800">+8.1%</p>
      <p class="text-sm text-slate-500">vs yesterday</p>
    </div>
    <p class="sr-only">Revenue rose through the day, with the peak in the last hour.</p>
    <div class="mt-5 flex h-28 items-end gap-1.5" aria-hidden="true">
      <div class="h-[20%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[35%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[30%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[55%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[70%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[48%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[62%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[80%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[66%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[90%] flex-1 rounded-t-md bg-emerald-600"></div>
    </div>
  </div>
  <div role="tabpanel" id="ttt-sg-panel-2" aria-labelledby="ttt-sg-tab-2" tabindex="0" hidden class="mt-5 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-700">
    <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
      <p class="text-3xl font-semibold tracking-tight text-slate-900 tabular-nums">$8,940</p>
      <p class="rounded-full px-2 py-0.5 text-xs font-semibold bg-emerald-50 text-emerald-800">+12.4%</p>
      <p class="text-sm text-slate-500">vs last week</p>
    </div>
    <p class="sr-only">Weekend days brought the most revenue this week.</p>
    <div class="mt-5 flex h-28 items-end gap-1.5" aria-hidden="true">
      <div class="h-[52%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[60%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[45%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[72%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[66%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[88%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[95%] flex-1 rounded-t-md bg-emerald-600"></div>
    </div>
  </div>
  <div role="tabpanel" id="ttt-sg-panel-3" aria-labelledby="ttt-sg-tab-3" tabindex="0" hidden class="mt-5 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-700">
    <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
      <p class="text-3xl font-semibold tracking-tight text-slate-900 tabular-nums">$36,410</p>
      <p class="rounded-full px-2 py-0.5 text-xs font-semibold bg-rose-50 text-rose-800">-3.2%</p>
      <p class="text-sm text-slate-500">vs September</p>
    </div>
    <p class="sr-only">Revenue dipped slightly in the second half of the month.</p>
    <div class="mt-5 flex h-28 items-end gap-1.5" aria-hidden="true">
      <div class="h-[70%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[64%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[58%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[75%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[52%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[60%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[48%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[55%] flex-1 rounded-t-md bg-emerald-600"></div>
    </div>
  </div>
  <div role="tabpanel" id="ttt-sg-panel-4" aria-labelledby="ttt-sg-tab-4" tabindex="0" hidden class="mt-5 rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-700">
    <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
      <p class="text-3xl font-semibold tracking-tight text-slate-900 tabular-nums">$402,700</p>
      <p class="rounded-full px-2 py-0.5 text-xs font-semibold bg-emerald-50 text-emerald-800">+21.6%</p>
      <p class="text-sm text-slate-500">vs 2025</p>
    </div>
    <p class="sr-only">Revenue grew almost every month, ending the year at its highest point.</p>
    <div class="mt-5 flex h-28 items-end gap-1.5" aria-hidden="true">
      <div class="h-[30%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[34%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[40%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[38%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[45%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[52%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[50%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[58%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[64%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[70%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[86%] flex-1 rounded-t-md bg-emerald-200"></div>
      <div class="h-[96%] flex-1 rounded-t-md bg-emerald-600"></div>
    </div>
  </div>
</div>

Tabs with icons and count badges

  • group-aria-selected:
  • sr-only sm:not-sr-only
  • inline-flex gap-2
  • currentColor

Click a tab or use the arrow keys.

Each tab is a group, so group-aria-selected: recolors the badge inside it, while the SVG icons draw with currentColor and follow the tab color for free. On phones the labels collapse to sr-only and come back with sm:not-sr-only, so the tab keeps its accessible name even when only the icon and count show. Each badge carries sr-only text, which makes the name read as "Inbox 12 unread" instead of a bare number.

View code
<div class="ttt-tabs w-full max-w-2xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <h3 id="ttt-ib-label" class="text-lg font-semibold text-slate-900">Support desk</h3>
  <div role="tablist" aria-labelledby="ttt-ib-label" class="mt-3 flex justify-between gap-4 border-b border-slate-200 sm:justify-start sm:gap-7">
    <button type="button" role="tab" id="ttt-ib-tab-1" aria-controls="ttt-ib-panel-1" aria-selected="true" tabindex="0" class="group -mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-indigo-600 aria-selected:text-indigo-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 13h4.5l1.5 2.5h6l1.5-2.5H21M5.2 5.6 3 13v5a1.5 1.5 0 0 0 1.5 1.5h15A1.5 1.5 0 0 0 21 18v-5l-2.2-7.4A1.5 1.5 0 0 0 17.4 4.5H6.6a1.5 1.5 0 0 0-1.4 1.1Z"/></svg><span class="sr-only sm:not-sr-only">Inbox</span><span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-selected:bg-indigo-100 group-aria-selected:text-indigo-800">12<span class="sr-only"> unread</span></span></button>
    <button type="button" role="tab" id="ttt-ib-tab-2" aria-controls="ttt-ib-panel-2" aria-selected="false" tabindex="-1" class="group -mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-indigo-600 aria-selected:text-indigo-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Zm0 0v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-3.5 7.1"/></svg><span class="sr-only sm:not-sr-only">Mentions</span><span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-selected:bg-indigo-100 group-aria-selected:text-indigo-800">3<span class="sr-only"> unread</span></span></button>
    <button type="button" role="tab" id="ttt-ib-tab-3" aria-controls="ttt-ib-panel-3" aria-selected="false" tabindex="-1" class="group -mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-indigo-600 aria-selected:text-indigo-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15.75 7.5a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.5 20.1a7.5 7.5 0 0 1 15 0"/></svg><span class="sr-only sm:not-sr-only">Assigned to me</span><span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-selected:bg-indigo-100 group-aria-selected:text-indigo-800">5<span class="sr-only"> open</span></span></button>
    <button type="button" role="tab" id="ttt-ib-tab-4" aria-controls="ttt-ib-panel-4" aria-selected="false" tabindex="-1" class="group -mb-px inline-flex cursor-pointer items-center gap-2 border-b-2 border-transparent px-1 py-3 text-sm font-medium whitespace-nowrap text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-indigo-600 aria-selected:text-indigo-700 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.75 7.5h16.5M5.25 7.5v10.5A1.5 1.5 0 0 0 6.75 19.5h10.5a1.5 1.5 0 0 0 1.5-1.5V7.5M3.75 4.5h16.5v3H3.75v-3ZM10 11.25h4"/></svg><span class="sr-only sm:not-sr-only">Archived</span></button>
  </div>
  <div role="tabpanel" id="ttt-ib-panel-1" aria-labelledby="ttt-ib-tab-1" tabindex="0" class="rounded-lg pt-2 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-indigo-600">
    <ul class="divide-y divide-slate-100">
      <li class="flex gap-3 py-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-indigo-700 text-xs font-semibold text-white" aria-hidden="true">RK</span><span class="min-w-0 flex-1"><span class="flex items-baseline justify-between gap-3"><span class="truncate text-[15px] font-medium text-slate-900">Refund for order 20377</span><span class="shrink-0 text-xs text-slate-500">9:41</span></span><span class="block truncate text-sm text-slate-600">The parcel came back yesterday, can you confirm the refund?</span></span></li>
      <li class="flex gap-3 py-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-emerald-700 text-xs font-semibold text-white" aria-hidden="true">JS</span><span class="min-w-0 flex-1"><span class="flex items-baseline justify-between gap-3"><span class="truncate text-[15px] font-medium text-slate-900">Invoice address change</span><span class="shrink-0 text-xs text-slate-500">8:02</span></span><span class="block truncate text-sm text-slate-600">We moved offices last month and need new invoices.</span></span></li>
      <li class="flex gap-3 py-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-amber-700 text-xs font-semibold text-white" aria-hidden="true">PM</span><span class="min-w-0 flex-1"><span class="flex items-baseline justify-between gap-3"><span class="truncate text-[15px] font-medium text-slate-900">Bulk discount question</span><span class="shrink-0 text-xs text-slate-500">Mon</span></span><span class="block truncate text-sm text-slate-600">Do you offer pricing for teams over 50 seats?</span></span></li>
    </ul>
  </div>
  <div role="tabpanel" id="ttt-ib-panel-2" aria-labelledby="ttt-ib-tab-2" tabindex="0" hidden class="rounded-lg pt-2 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-indigo-600">
    <ul class="divide-y divide-slate-100">
      <li class="flex gap-3 py-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-rose-700 text-xs font-semibold text-white" aria-hidden="true">AL</span><span class="min-w-0 flex-1"><span class="flex items-baseline justify-between gap-3"><span class="truncate text-[15px] font-medium text-slate-900">Ana mentioned you in Shipping delays</span><span class="shrink-0 text-xs text-slate-500">10:12</span></span><span class="block truncate text-sm text-slate-600">Can you take the customers in the north region?</span></span></li>
    </ul>
  </div>
  <div role="tabpanel" id="ttt-ib-panel-3" aria-labelledby="ttt-ib-tab-3" tabindex="0" hidden class="rounded-lg pt-2 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-indigo-600">
    <ul class="divide-y divide-slate-100">
      <li class="flex gap-3 py-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-indigo-700 text-xs font-semibold text-white" aria-hidden="true">RK</span><span class="min-w-0 flex-1"><span class="flex items-baseline justify-between gap-3"><span class="truncate text-[15px] font-medium text-slate-900">Refund for order 20377</span><span class="shrink-0 text-xs text-slate-500">9:41</span></span><span class="block truncate text-sm text-slate-600">Waiting on the warehouse scan.</span></span></li>
      <li class="flex gap-3 py-3"><span class="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-slate-700 text-xs font-semibold text-white" aria-hidden="true">BT</span><span class="min-w-0 flex-1"><span class="flex items-baseline justify-between gap-3"><span class="truncate text-[15px] font-medium text-slate-900">Login link expired</span><span class="shrink-0 text-xs text-slate-500">Tue</span></span><span class="block truncate text-sm text-slate-600">Customer asked for a new magic link.</span></span></li>
    </ul>
  </div>
  <div role="tabpanel" id="ttt-ib-panel-4" aria-labelledby="ttt-ib-tab-4" tabindex="0" hidden class="rounded-lg pt-2 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-indigo-600">
    <p class="py-6 text-center text-[15px] text-slate-500">Archived conversations stay searchable for 12 months.</p>
  </div>
</div>

Scrollable tabs with edge buttons

  • overflow-x-auto
  • scrollbar-none
  • motion-safe:scroll-smooth
  • disabled:opacity-40

Use the arrow buttons, or focus a tab and press Right or End.

Eleven pill tabs scroll in one row with overflow-x-auto and scrollbar-none, and the hidden scrollbar is replaced by two real buttons instead of a fade, so the overflow is visible and clickable. The script scrolls by most of the visible width, disables a button at its end (styled with disabled:opacity-40) and keeps the selected tab in view when arrow keys move past the edge. motion-safe:scroll-smooth makes those scrolls glide only when motion is welcome.

View code
<div class="ttt-tabs w-full max-w-2xl">
  <h3 id="ttt-ov-label" class="mb-3 text-lg font-semibold text-zinc-900">Shop by category</h3>
  <div class="flex items-center gap-2">
    <button type="button" class="ttt-prev inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full bg-white text-zinc-700 ring-1 ring-zinc-200 hover:bg-zinc-50 disabled:cursor-default disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600" aria-label="Scroll categories left"><svg class="size-5" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m12 5-5 5 5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
    <div role="tablist" aria-labelledby="ttt-ov-label" class="relative flex min-w-0 flex-1 gap-2 overflow-x-auto px-0.5 py-1 scrollbar-none motion-safe:scroll-smooth">
    <button type="button" role="tab" id="ttt-ov-tab-1" aria-controls="ttt-ov-panel-1" aria-selected="true" tabindex="0" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">All</button>
    <button type="button" role="tab" id="ttt-ov-tab-2" aria-controls="ttt-ov-panel-2" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Lighting</button>
    <button type="button" role="tab" id="ttt-ov-tab-3" aria-controls="ttt-ov-panel-3" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Furniture</button>
    <button type="button" role="tab" id="ttt-ov-tab-4" aria-controls="ttt-ov-panel-4" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Textiles</button>
    <button type="button" role="tab" id="ttt-ov-tab-5" aria-controls="ttt-ov-panel-5" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Ceramics</button>
    <button type="button" role="tab" id="ttt-ov-tab-6" aria-controls="ttt-ov-panel-6" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Kitchen</button>
    <button type="button" role="tab" id="ttt-ov-tab-7" aria-controls="ttt-ov-panel-7" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Outdoor</button>
    <button type="button" role="tab" id="ttt-ov-tab-8" aria-controls="ttt-ov-panel-8" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Rugs</button>
    <button type="button" role="tab" id="ttt-ov-tab-9" aria-controls="ttt-ov-panel-9" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Office</button>
    <button type="button" role="tab" id="ttt-ov-tab-10" aria-controls="ttt-ov-panel-10" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Kids</button>
    <button type="button" role="tab" id="ttt-ov-tab-11" aria-controls="ttt-ov-panel-11" aria-selected="false" tabindex="-1" class="shrink-0 cursor-pointer rounded-full border border-zinc-200 bg-white px-4 py-2 text-sm font-medium whitespace-nowrap text-zinc-700 hover:border-zinc-400 aria-selected:border-zinc-900 aria-selected:bg-zinc-900 aria-selected:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">Sale</button>
    </div>
    <button type="button" class="ttt-next inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full bg-white text-zinc-700 ring-1 ring-zinc-200 hover:bg-zinc-50 disabled:cursor-default disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600" aria-label="Scroll categories right"><svg class="size-5" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m8 5 5 5-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-1" aria-labelledby="ttt-ov-tab-1" class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">All</span>, 412 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">New in</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Best sellers</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Gift ideas</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-2" aria-labelledby="ttt-ov-tab-2" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Lighting</span>, 42 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Pendants</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Table lamps</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Floor lamps</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-3" aria-labelledby="ttt-ov-tab-3" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Furniture</span>, 58 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Chairs</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Side tables</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Shelving</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-4" aria-labelledby="ttt-ov-tab-4" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Textiles</span>, 77 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Throws</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Cushions</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Linen</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-5" aria-labelledby="ttt-ov-tab-5" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Ceramics</span>, 36 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Vases</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Mugs</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Planters</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-6" aria-labelledby="ttt-ov-tab-6" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Kitchen</span>, 64 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Knives</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Boards</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Storage jars</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-7" aria-labelledby="ttt-ov-tab-7" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Outdoor</span>, 23 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Lanterns</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Planters</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Cushions</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-8" aria-labelledby="ttt-ov-tab-8" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Rugs</span>, 19 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Wool</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Jute</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Runners</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-9" aria-labelledby="ttt-ov-tab-9" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Office</span>, 31 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Desk lamps</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Organizers</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Notebooks</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-10" aria-labelledby="ttt-ov-tab-10" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Kids</span>, 27 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Bedding</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Toys</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Night lights</a></li></ul>
  </div>
  <div role="tabpanel" id="ttt-ov-panel-11" aria-labelledby="ttt-ov-tab-11" hidden class="mt-4 rounded-xl bg-white p-5 ring-1 ring-zinc-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600">
    <p class="text-[15px] text-zinc-600"><span class="font-semibold text-zinc-900">Sale</span>, 48 products</p>
    <ul class="mt-3 flex flex-wrap gap-2"><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Up to 30% off</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Last pieces</a></li><li><a href="#" class="inline-flex rounded-full bg-orange-50 px-3 py-1.5 text-sm font-medium text-orange-900 hover:bg-orange-100 focus-visible:outline-2 focus-visible:outline-orange-600">Bundles</a></li></ul>
  </div>
</div>

Full-width tabs with a disabled step and fade-in panels

  • flex-1
  • aria-disabled:
  • starting:opacity-0
  • motion-reduce:transition-none

Open Return, choose a flight, and the Seats tab unlocks.

flex-1 on every tab splits the track into equal columns, and each tab stacks a label and a date with flex-col. The Seats tab starts with aria-disabled="true": aria-disabled: variants grey it out, and the script skips it on click and in arrow-key movement until a return flight is chosen. Panels fade in with transition-opacity and starting:opacity-0, which works even though the hidden attribute sets display: none, and motion-reduce:transition-none turns the fade off.

View code
<div class="ttt-tabs w-full max-w-lg rounded-2xl bg-white p-4 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <h3 id="ttt-fl-label" class="text-lg font-semibold text-slate-900">Oslo to Lisbon</h3>
  <div role="tablist" aria-labelledby="ttt-fl-label" class="mt-3 flex gap-1 rounded-xl bg-violet-50 p-1">
    <button type="button" role="tab" id="ttt-fl-tab-1" aria-controls="ttt-fl-panel-1" aria-selected="true" tabindex="0" class="flex flex-1 cursor-pointer flex-col items-center rounded-lg px-2 py-2 text-sm font-semibold text-slate-600 hover:text-slate-900 aria-selected:bg-white aria-selected:text-violet-800 aria-selected:shadow-xs aria-disabled:cursor-not-allowed aria-disabled:text-slate-400 aria-disabled:hover:text-slate-400 focus-visible:outline-2 focus-visible:outline-violet-700">Outbound<span class="text-xs font-medium text-slate-500">Tue 18 Nov</span></button>
    <button type="button" role="tab" id="ttt-fl-tab-2" aria-controls="ttt-fl-panel-2" aria-selected="false" tabindex="-1" class="flex flex-1 cursor-pointer flex-col items-center rounded-lg px-2 py-2 text-sm font-semibold text-slate-600 hover:text-slate-900 aria-selected:bg-white aria-selected:text-violet-800 aria-selected:shadow-xs aria-disabled:cursor-not-allowed aria-disabled:text-slate-400 aria-disabled:hover:text-slate-400 focus-visible:outline-2 focus-visible:outline-violet-700">Return<span class="text-xs font-medium text-slate-500">Sun 23 Nov</span></button>
    <button type="button" role="tab" id="ttt-fl-tab-3" aria-disabled="true" aria-controls="ttt-fl-panel-3" aria-selected="false" tabindex="-1" class="flex flex-1 cursor-pointer flex-col items-center rounded-lg px-2 py-2 text-sm font-semibold text-slate-600 hover:text-slate-900 aria-selected:bg-white aria-selected:text-violet-800 aria-selected:shadow-xs aria-disabled:cursor-not-allowed aria-disabled:text-slate-400 aria-disabled:hover:text-slate-400 focus-visible:outline-2 focus-visible:outline-violet-700">Seats<span class="text-xs font-medium text-slate-500">Pick a return first</span></button>
  </div>
  <div role="tabpanel" id="ttt-fl-panel-1" aria-labelledby="ttt-fl-tab-1" tabindex="0" class="mt-4 rounded-xl transition-opacity duration-300 starting:opacity-0 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">
    <ul class="space-y-3"><li class="flex flex-col gap-2 rounded-xl border border-slate-200 p-4 sm:flex-row sm:items-center sm:justify-between"><span><span class="block text-lg font-semibold tabular-nums text-slate-900">07:10 &ndash; 11:45</span><span class="text-sm text-slate-500">OSL to LIS, nonstop 4h 35m, &euro;142</span></span><span class="text-sm font-medium text-emerald-700">Selected</span></li></ul>
  </div>
  <div role="tabpanel" id="ttt-fl-panel-2" aria-labelledby="ttt-fl-tab-2" hidden class="mt-4 rounded-xl transition-opacity duration-300 starting:opacity-0 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">
    <ul class="space-y-3"><li class="flex flex-col gap-2 rounded-xl border border-slate-200 p-4 sm:flex-row sm:items-center sm:justify-between"><span><span class="block text-lg font-semibold tabular-nums text-slate-900">12:30 &ndash; 19:20</span><span class="text-sm text-slate-500">LIS to OSL, nonstop 4h 50m, &euro;128</span></span><button type="button" class="ttt-pick mt-3 w-full cursor-pointer rounded-lg bg-violet-700 px-4 py-2.5 text-sm font-semibold text-white hover:bg-violet-800 disabled:cursor-default disabled:bg-emerald-700 sm:mt-0 sm:w-auto focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">Choose</button></li><li class="flex flex-col gap-2 rounded-xl border border-slate-200 p-4 sm:flex-row sm:items-center sm:justify-between"><span><span class="block text-lg font-semibold tabular-nums text-slate-900">18:05 &ndash; 23:10</span><span class="text-sm text-slate-500">LIS to OSL, nonstop 4h 05m, &euro;156</span></span><button type="button" class="ttt-pick mt-3 w-full cursor-pointer rounded-lg bg-violet-700 px-4 py-2.5 text-sm font-semibold text-white hover:bg-violet-800 disabled:cursor-default disabled:bg-emerald-700 sm:mt-0 sm:w-auto focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">Choose</button></li></ul>
  </div>
  <div role="tabpanel" id="ttt-fl-panel-3" aria-labelledby="ttt-fl-tab-3" tabindex="0" hidden class="mt-4 rounded-xl transition-opacity duration-300 starting:opacity-0 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">
    <p class="text-[15px] text-slate-600">Seat <span class="font-semibold text-slate-900">14A</span> is held for you on both flights. Window, extra legroom.</p>
    <div class="mt-4 grid grid-cols-6 gap-1.5" aria-hidden="true">
      <span class="h-8 rounded-md bg-violet-700"></span><span class="h-8 rounded-md bg-slate-200"></span><span class="h-8 rounded-md bg-slate-100"></span><span class="h-8 rounded-md bg-slate-200"></span><span class="h-8 rounded-md bg-slate-100"></span><span class="h-8 rounded-md bg-slate-200"></span>
      <span class="h-8 rounded-md bg-slate-100"></span><span class="h-8 rounded-md bg-slate-200"></span><span class="h-8 rounded-md bg-slate-200"></span><span class="h-8 rounded-md bg-slate-100"></span><span class="h-8 rounded-md bg-slate-200"></span><span class="h-8 rounded-md bg-slate-100"></span>
    </div>
  </div>
</div>

Dark mode tabs with a theme switch

  • dark:
  • dark:aria-selected:
  • @custom-variant dark
  • aria-pressed

Use the theme switch, then the tabs.

Every color class has a dark: twin, including stacked variants like dark:aria-selected:text-cyan-300 for the active tab. The demo uses the class strategy (@custom-variant dark (&:where(.dark, .dark *)) in your CSS), so the switch only toggles a dark class on the wrapper and flips aria-pressed; everything inside restyles itself. Without that line, dark: follows prefers-color-scheme instead.

View code
<div class="ttt-tabs ttt-scheme dark w-full max-w-2xl">
  <div class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6 dark:bg-slate-900 dark:ring-white/10">
    <div class="flex items-center justify-between gap-3">
      <h3 id="ttt-dk-label" class="text-lg font-semibold text-slate-900 dark:text-white">Deploy #482 logs</h3>
      <button type="button" class="ttt-theme inline-flex h-10 cursor-pointer items-center gap-2 rounded-full bg-slate-100 px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-400" aria-pressed="true">
        <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" stroke-linejoin="round"/></svg>
        Dark theme
      </button>
    </div>
    <div role="tablist" aria-labelledby="ttt-dk-label" class="mt-3 flex gap-6 border-b border-slate-200 dark:border-slate-700">
    <button type="button" role="tab" id="ttt-dk-tab-1" aria-controls="ttt-dk-panel-1" aria-selected="true" tabindex="0" class="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm font-medium text-slate-500 hover:text-slate-800 aria-selected:border-cyan-600 aria-selected:text-cyan-800 dark:text-slate-400 dark:hover:text-slate-200 dark:aria-selected:border-cyan-400 dark:aria-selected:text-cyan-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-400">Build</button>
    <button type="button" role="tab" id="ttt-dk-tab-2" aria-controls="ttt-dk-panel-2" aria-selected="false" tabindex="-1" class="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm font-medium text-slate-500 hover:text-slate-800 aria-selected:border-cyan-600 aria-selected:text-cyan-800 dark:text-slate-400 dark:hover:text-slate-200 dark:aria-selected:border-cyan-400 dark:aria-selected:text-cyan-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-400">Functions</button>
    <button type="button" role="tab" id="ttt-dk-tab-3" aria-controls="ttt-dk-panel-3" aria-selected="false" tabindex="-1" class="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm font-medium text-slate-500 hover:text-slate-800 aria-selected:border-cyan-600 aria-selected:text-cyan-800 dark:text-slate-400 dark:hover:text-slate-200 dark:aria-selected:border-cyan-400 dark:aria-selected:text-cyan-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-400">Edge</button>
    </div>
    <div role="tabpanel" id="ttt-dk-panel-1" aria-labelledby="ttt-dk-tab-1" tabindex="0" class="mt-4 overflow-x-auto rounded-lg bg-slate-50 p-4 font-mono text-[13px] leading-6 text-slate-700 dark:bg-slate-950 dark:text-slate-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-400">
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:04:11</span> Installing dependencies</p>
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:04:38</span> Compiled 214 modules <span class="text-emerald-700 dark:text-emerald-400">ok</span></p>
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:04:52</span> Generated CSS, 38.2 kB <span class="text-emerald-700 dark:text-emerald-400">ok</span></p>
    </div>
    <div role="tabpanel" id="ttt-dk-panel-2" aria-labelledby="ttt-dk-tab-2" tabindex="0" hidden class="mt-4 overflow-x-auto rounded-lg bg-slate-50 p-4 font-mono text-[13px] leading-6 text-slate-700 dark:bg-slate-950 dark:text-slate-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-400">
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:05:03</span> Bundled 6 functions <span class="text-emerald-700 dark:text-emerald-400">ok</span></p>
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:05:04</span> /api/checkout cold start 182 ms <span class="text-amber-700 dark:text-amber-300">warn</span></p>
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:05:05</span> Region fra1 <span class="text-emerald-700 dark:text-emerald-400">ok</span></p>
    </div>
    <div role="tabpanel" id="ttt-dk-panel-3" aria-labelledby="ttt-dk-tab-3" tabindex="0" hidden class="mt-4 overflow-x-auto rounded-lg bg-slate-50 p-4 font-mono text-[13px] leading-6 text-slate-700 dark:bg-slate-950 dark:text-slate-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600 dark:focus-visible:outline-cyan-400">
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:05:09</span> Cache purged on 18 locations <span class="text-emerald-700 dark:text-emerald-400">ok</span></p>
    <p class="whitespace-nowrap"><span class="text-slate-500 dark:text-slate-500">12:05:12</span> Edge config synced <span class="text-emerald-700 dark:text-emerald-400">ok</span></p>
    </div>
  </div>
</div>

Tabs in real interfaces

Tabs inside the components where they usually live: a product page, a pricing table, a code block and a settings card.

Product detail tabs

  • md:grid-cols-[2fr_3fr]
  • shadow-[inset_0_-1px_0_var(--color-stone-200)]
  • overflow-x-auto
  • aria-selected:border-amber-700

Click a tab or use the arrow keys.

The baseline under the tabs is an inset box shadow drawn with an arbitrary value and the stone-200 theme variable, not a border, so the row can use overflow-x-auto on phones without clipping the active border-b-2. The card switches from a stacked photo to a two-column grid with md:grid-cols-[2fr_3fr], and the rating badge inside the Reviews tab is part of the tab name.

View code
<article class="ttt-tabs grid w-full max-w-3xl overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-stone-200 md:grid-cols-[2fr_3fr]">
  <img src="fjord.webp" width="800" height="600" alt="Deep blue fjord between granite cliffs, seen from a rocky ledge" class="aspect-[4/3] h-full w-full object-cover md:aspect-auto">
  <div class="min-w-0 p-5 sm:p-6">
    <p class="text-sm font-medium text-amber-800">Full day, moderate</p>
    <h3 id="ttt-pd-label" class="mt-1 text-xl font-semibold text-stone-900">Cliff-top fjord hike</h3>
    <p class="mt-1 text-stone-600"><span class="text-lg font-semibold text-stone-900">&euro;89</span> per person</p>
    <div role="tablist" aria-labelledby="ttt-pd-label" class="relative mt-4 flex gap-5 overflow-x-auto shadow-[inset_0_-1px_0_var(--color-stone-200)] scrollbar-none">
    <button type="button" role="tab" id="ttt-pd-tab-1" aria-controls="ttt-pd-panel-1" aria-selected="true" tabindex="0" class="inline-flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 border-transparent py-3 text-sm font-medium whitespace-nowrap text-stone-500 hover:text-stone-800 aria-selected:border-amber-700 aria-selected:text-stone-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-700">Itinerary</button>
    <button type="button" role="tab" id="ttt-pd-tab-2" aria-controls="ttt-pd-panel-2" aria-selected="false" tabindex="-1" class="inline-flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 border-transparent py-3 text-sm font-medium whitespace-nowrap text-stone-500 hover:text-stone-800 aria-selected:border-amber-700 aria-selected:text-stone-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-700">Included</button>
    <button type="button" role="tab" id="ttt-pd-tab-3" aria-controls="ttt-pd-panel-3" aria-selected="false" tabindex="-1" class="inline-flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 border-transparent py-3 text-sm font-medium whitespace-nowrap text-stone-500 hover:text-stone-800 aria-selected:border-amber-700 aria-selected:text-stone-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-700">Reviews <span class="rounded-full bg-amber-100 px-1.5 py-0.5 text-xs font-semibold text-amber-900">4.8</span></button>
    <button type="button" role="tab" id="ttt-pd-tab-4" aria-controls="ttt-pd-panel-4" aria-selected="false" tabindex="-1" class="inline-flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 border-transparent py-3 text-sm font-medium whitespace-nowrap text-stone-500 hover:text-stone-800 aria-selected:border-amber-700 aria-selected:text-stone-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-700">Meeting point</button>
    </div>
    <div role="tabpanel" id="ttt-pd-panel-1" aria-labelledby="ttt-pd-tab-1" tabindex="0" class="pt-4 text-[15px] leading-relaxed text-stone-600 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-700 rounded-md">
      <ol class="space-y-2">
        <li><span class="font-semibold text-stone-900">08:30</span> Ferry and bus to the trailhead</li>
        <li><span class="font-semibold text-stone-900">10:00</span> Climb through pine forest, 350 m of ascent</li>
        <li><span class="font-semibold text-stone-900">12:30</span> Lunch on the plateau above the fjord</li>
        <li><span class="font-semibold text-stone-900">16:30</span> Back in the harbor</li>
      </ol>
    </div>
    <div role="tabpanel" id="ttt-pd-panel-2" aria-labelledby="ttt-pd-tab-2" tabindex="0" hidden class="pt-4 text-[15px] leading-relaxed text-stone-600 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-700 rounded-md">
      <ul class="space-y-2">
        <li class="flex gap-2"><svg class="mt-1 size-4 shrink-0 text-amber-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Certified mountain guide, groups of 12 or fewer</li>
        <li class="flex gap-2"><svg class="mt-1 size-4 shrink-0 text-amber-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Ferry and bus tickets</li>
        <li class="flex gap-2"><svg class="mt-1 size-4 shrink-0 text-amber-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Packed lunch and hot drinks</li>
      </ul>
    </div>
    <div role="tabpanel" id="ttt-pd-panel-3" aria-labelledby="ttt-pd-tab-3" tabindex="0" hidden class="pt-4 text-[15px] leading-relaxed text-stone-600 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-700 rounded-md">
      <p class="flex items-center gap-1"><svg class="size-4 text-amber-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 2.5l2.3 4.8 5.2.7-3.8 3.6.9 5.2L10 14.3l-4.6 2.5.9-5.2L2.5 8l5.2-.7Z"/></svg><svg class="size-4 text-amber-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 2.5l2.3 4.8 5.2.7-3.8 3.6.9 5.2L10 14.3l-4.6 2.5.9-5.2L2.5 8l5.2-.7Z"/></svg><svg class="size-4 text-amber-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 2.5l2.3 4.8 5.2.7-3.8 3.6.9 5.2L10 14.3l-4.6 2.5.9-5.2L2.5 8l5.2-.7Z"/></svg><svg class="size-4 text-amber-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 2.5l2.3 4.8 5.2.7-3.8 3.6.9 5.2L10 14.3l-4.6 2.5.9-5.2L2.5 8l5.2-.7Z"/></svg><svg class="size-4 text-amber-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 2.5l2.3 4.8 5.2.7-3.8 3.6.9 5.2L10 14.3l-4.6 2.5.9-5.2L2.5 8l5.2-.7Z"/></svg><span class="ml-1 font-semibold text-stone-900">4.8</span><span class="text-stone-500">from 312 reviews</span></p>
      <blockquote class="mt-3 border-l-2 border-amber-200 pl-3">The view at lunch alone was worth it, and our guide knew every plant on the trail. <span class="mt-1 block text-sm text-stone-500">Hanna, October 2026</span></blockquote>
    </div>
    <div role="tabpanel" id="ttt-pd-panel-4" aria-labelledby="ttt-pd-tab-4" tabindex="0" hidden class="pt-4 text-[15px] leading-relaxed text-stone-600 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-700 rounded-md">
      <p>Meet at the ferry kiosk on Quay 3, Harbor Street 12, at 08:15. Look for the guide with the orange flag.</p>
      <p class="mt-2 text-sm text-stone-500">Questions: +1 555 0142</p>
    </div>
  </div>
</article>

Monthly and yearly pricing tabs

  • @theme
  • bg-ttt-moss/10
  • aria-selected:bg-ttt-moss
  • md:grid-cols-3

Switch between Monthly and Yearly.

A custom @theme color, --color-ttt-moss, generates bg-ttt-moss, text-ttt-moss, ring-ttt-moss and opacity steps such as hover:bg-ttt-moss/10, so the whole card set uses one brand token. Both billing tabs control the same tabpanel; the script swaps each price from its data-monthly or data-yearly attribute and points aria-labelledby at the active tab. Tabs fit here because the period changes what the panel shows; a single on/off choice elsewhere would be a switch.

View code
<section class="ttt-tabs w-full max-w-4xl text-center">
  <h3 id="ttt-pr-label" class="text-2xl font-semibold tracking-tight text-ttt-moss">Plans for growing studios</h3>
  <p class="mt-2 text-stone-600">Switch to yearly billing and keep two and a half months.</p>
  <div role="tablist" aria-label="Billing period" class="mt-5 inline-flex rounded-full bg-white p-1 ring-1 ring-stone-200">
    <button type="button" role="tab" id="ttt-pr-tab-1" data-period="monthly" aria-controls="ttt-pr-panel-1" aria-selected="true" tabindex="0" class="inline-flex cursor-pointer items-center gap-2 rounded-full px-5 py-2.5 text-sm font-semibold text-ttt-moss hover:bg-ttt-moss/10 aria-selected:bg-ttt-moss aria-selected:text-white aria-selected:hover:bg-ttt-moss focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttt-moss">Monthly</button>
    <button type="button" role="tab" id="ttt-pr-tab-2" data-period="yearly" aria-controls="ttt-pr-panel-1" aria-selected="false" tabindex="-1" class="inline-flex cursor-pointer items-center gap-2 rounded-full px-5 py-2.5 text-sm font-semibold text-ttt-moss hover:bg-ttt-moss/10 aria-selected:bg-ttt-moss aria-selected:text-white aria-selected:hover:bg-ttt-moss focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttt-moss">Yearly <span class="rounded-full bg-lime-200 px-2 py-0.5 text-xs font-semibold text-ttt-moss">Save 20%</span></button>
  </div>
  <div role="tabpanel" id="ttt-pr-panel-1" aria-labelledby="ttt-pr-tab-1" class="mt-8 grid gap-4 text-left md:grid-cols-3">
      <div class="rounded-2xl p-6 ring-1 bg-white text-ttt-moss ring-stone-200">
        <h4 class="text-base font-semibold">Starter</h4>
        <p class="mt-1 text-sm text-stone-600">For freelancers and side projects.</p>
        <p class="mt-4 flex items-baseline gap-1"><span class="ttt-price text-4xl font-semibold tracking-tight tabular-nums" data-monthly="$12" data-yearly="$10">$12</span><span class="ttt-per text-sm text-stone-600">/mo</span></p>
        <a href="#" class="mt-5 block rounded-full px-4 py-2.5 text-center text-sm font-semibold bg-ttt-moss text-white hover:bg-ttt-moss/90 focus-visible:outline-ttt-moss focus-visible:outline-2 focus-visible:outline-offset-2">Choose Starter</a>
        <ul class="mt-5 space-y-2 text-sm text-stone-600"><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-emerald-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>3 active projects</li><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-emerald-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Client portal</li><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-emerald-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Email support</li></ul>
      </div>
      <div class="rounded-2xl p-6 ring-1 bg-ttt-moss text-white ring-ttt-moss">
        <h4 class="text-base font-semibold">Studio</h4>
        <p class="mt-1 text-sm text-lime-100">For small teams that bill by the hour.</p>
        <p class="mt-4 flex items-baseline gap-1"><span class="ttt-price text-4xl font-semibold tracking-tight tabular-nums" data-monthly="$29" data-yearly="$23">$29</span><span class="ttt-per text-sm text-lime-100">/mo</span></p>
        <a href="#" class="mt-5 block rounded-full px-4 py-2.5 text-center text-sm font-semibold bg-lime-300 text-ttt-moss hover:bg-lime-200 focus-visible:outline-lime-300 focus-visible:outline-2 focus-visible:outline-offset-2">Choose Studio</a>
        <ul class="mt-5 space-y-2 text-sm text-lime-100"><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-lime-300" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Unlimited projects</li><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-lime-300" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Time tracking and invoices</li><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-lime-300" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Priority support</li></ul>
      </div>
      <div class="rounded-2xl p-6 ring-1 bg-white text-ttt-moss ring-stone-200">
        <h4 class="text-base font-semibold">Agency</h4>
        <p class="mt-1 text-sm text-stone-600">For agencies with several teams.</p>
        <p class="mt-4 flex items-baseline gap-1"><span class="ttt-price text-4xl font-semibold tracking-tight tabular-nums" data-monthly="$79" data-yearly="$63">$79</span><span class="ttt-per text-sm text-stone-600">/mo</span></p>
        <a href="#" class="mt-5 block rounded-full px-4 py-2.5 text-center text-sm font-semibold bg-ttt-moss text-white hover:bg-ttt-moss/90 focus-visible:outline-ttt-moss focus-visible:outline-2 focus-visible:outline-offset-2">Choose Agency</a>
        <ul class="mt-5 space-y-2 text-sm text-stone-600"><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-emerald-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Everything in Studio</li><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-emerald-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Team permissions</li><li class="flex gap-2"><svg class="mt-0.5 size-4 shrink-0 text-emerald-700" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m4.5 10.5 3.5 3.5 7.5-8" stroke-linecap="round" stroke-linejoin="round"/></svg>Single sign-on</li></ul>
      </div>
  </div>
</section>

Code sample tabs with a copy button

  • bg-[#0d1117]
  • ring-white/10
  • font-mono
  • aria-live

Switch the language, then press Copy.

An arbitrary background, bg-[#0d1117], and translucent white rings and borders (ring-white/10) give the editor look without a custom theme. The pre inside each panel scrolls on its own with overflow-auto and takes tabindex="0", so keyboard users can scroll long samples. The copy button is simulated here: it selects the visible sample and announces the result through an aria-live region; on a live site you would call navigator.clipboard.writeText with the code text.

View code
<div class="ttt-tabs w-full max-w-2xl overflow-hidden rounded-xl bg-[#0d1117] ring-1 ring-white/10">
  <div class="flex items-center justify-between gap-2 border-b border-white/10 pr-2 pl-2">
    <div role="tablist" aria-label="Code language" class="flex">
    <button type="button" role="tab" id="ttt-cd-tab-1" aria-controls="ttt-cd-panel-1" aria-selected="true" tabindex="0" class="-mb-px cursor-pointer border-b-2 border-transparent px-3 py-3 font-mono text-[13px] font-medium text-slate-400 hover:text-slate-200 aria-selected:border-lime-400 aria-selected:text-white focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-lime-400">HTML</button>
    <button type="button" role="tab" id="ttt-cd-tab-2" aria-controls="ttt-cd-panel-2" aria-selected="false" tabindex="-1" class="-mb-px cursor-pointer border-b-2 border-transparent px-3 py-3 font-mono text-[13px] font-medium text-slate-400 hover:text-slate-200 aria-selected:border-lime-400 aria-selected:text-white focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-lime-400">React</button>
    <button type="button" role="tab" id="ttt-cd-tab-3" aria-controls="ttt-cd-panel-3" aria-selected="false" tabindex="-1" class="-mb-px cursor-pointer border-b-2 border-transparent px-3 py-3 font-mono text-[13px] font-medium text-slate-400 hover:text-slate-200 aria-selected:border-lime-400 aria-selected:text-white focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-lime-400">Vue</button>
    </div>
    <button type="button" class="ttt-copy inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-md px-2.5 text-[13px] font-medium text-slate-300 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-lime-400">
      <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="8.5" y="8.5" width="11" height="11" rx="2"/><path d="M15.5 8.5V6a1.5 1.5 0 0 0-1.5-1.5H6A1.5 1.5 0 0 0 4.5 6v8A1.5 1.5 0 0 0 6 15.5h2.5"/></svg>
      <span class="ttt-copy-label">Copy</span>
    </button>
    <span class="ttt-copy-status sr-only" aria-live="polite"></span>
  </div>
  <div role="tabpanel" id="ttt-cd-panel-1" aria-labelledby="ttt-cd-tab-1">
    <pre tabindex="0" aria-label="HTML example" class="max-h-72 overflow-auto p-4 text-[13px] leading-6 text-slate-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-lime-400"><code>&lt;<span class="text-sky-300">div</span> <span class="text-lime-300">role</span>=<span class="text-amber-200">"tablist"</span> <span class="text-lime-300">aria-label</span>=<span class="text-amber-200">"Plans"</span>&gt;
  &lt;<span class="text-sky-300">button</span> <span class="text-lime-300">role</span>=<span class="text-amber-200">"tab"</span> <span class="text-lime-300">aria-selected</span>=<span class="text-amber-200">"true"</span>
    <span class="text-lime-300">class</span>=<span class="text-amber-200">"aria-selected:text-lime-700"</span>&gt;Monthly&lt;/<span class="text-sky-300">button</span>&gt;
&lt;/<span class="text-sky-300">div</span>&gt;
&lt;<span class="text-sky-300">div</span> <span class="text-lime-300">role</span>=<span class="text-amber-200">"tabpanel"</span> <span class="text-lime-300">id</span>=<span class="text-amber-200">"monthly"</span>&gt;...&lt;/<span class="text-sky-300">div</span>&gt;</code></pre>
  </div>
  <div role="tabpanel" id="ttt-cd-panel-2" aria-labelledby="ttt-cd-tab-2" hidden>
    <pre tabindex="0" aria-label="React example" class="max-h-72 overflow-auto p-4 text-[13px] leading-6 text-slate-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-lime-400"><code><span class="text-fuchsia-300">import</span> { TabGroup, TabList, Tab } <span class="text-fuchsia-300">from</span> <span class="text-amber-200">'@headlessui/react'</span>

&lt;<span class="text-sky-300">TabGroup</span>&gt;
  &lt;<span class="text-sky-300">TabList</span>&gt;
    &lt;<span class="text-sky-300">Tab</span> <span class="text-lime-300">className</span>=<span class="text-amber-200">"data-selected:text-lime-700"</span>&gt;
      Monthly
    &lt;/<span class="text-sky-300">Tab</span>&gt;
  &lt;/<span class="text-sky-300">TabList</span>&gt;
&lt;/<span class="text-sky-300">TabGroup</span>&gt;</code></pre>
  </div>
  <div role="tabpanel" id="ttt-cd-panel-3" aria-labelledby="ttt-cd-tab-3" hidden>
    <pre tabindex="0" aria-label="Vue example" class="max-h-72 overflow-auto p-4 text-[13px] leading-6 text-slate-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-lime-400"><code>&lt;<span class="text-sky-300">template</span>&gt;
  &lt;<span class="text-sky-300">button</span> <span class="text-lime-300">role</span>=<span class="text-amber-200">"tab"</span>&gt;
    <span class="text-lime-300">:aria-selected</span>=<span class="text-amber-200">"active === 'monthly'"</span>
    <span class="text-lime-300">@click</span>=<span class="text-amber-200">"active = 'monthly'"</span>
    <span class="text-lime-300">class</span>=<span class="text-amber-200">"aria-selected:text-lime-700"</span>&gt;
    Monthly
  &lt;/<span class="text-sky-300">button</span>&gt;
&lt;/<span class="text-sky-300">template</span>&gt;</code></pre>
  </div>
</div>

Settings tabs in a card

  • role="switch"
  • peer-checked:after:translate-x-5
  • placeholder:
  • sm:col-span-2

Switch tabs, change a setting and save.

The tabs sit in the card header and share its bottom border, so the active underline reads as part of the card. Each panel holds its own form; the switches are real checkboxes with role="switch", styled through a sibling span with peer-checked: and an after: knob that slides with motion-safe:after:transition-transform. Submitting is blocked in the demo and a role="status" line confirms the save.

View code
<div class="ttt-tabs w-full max-w-xl overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200">
  <div class="border-b border-slate-200 px-5 pt-5 sm:px-6">
    <h3 id="ttt-st-label" class="text-lg font-semibold text-slate-900">Settings</h3>
    <div role="tablist" aria-labelledby="ttt-st-label" class="mt-2 flex gap-6">
    <button type="button" role="tab" id="ttt-st-tab-1" aria-controls="ttt-st-panel-1" aria-selected="true" tabindex="0" class="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm font-medium text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-fuchsia-700 aria-selected:text-fuchsia-800 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-fuchsia-700">Profile</button>
    <button type="button" role="tab" id="ttt-st-tab-2" aria-controls="ttt-st-panel-2" aria-selected="false" tabindex="-1" class="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm font-medium text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-fuchsia-700 aria-selected:text-fuchsia-800 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-fuchsia-700">Notifications</button>
    <button type="button" role="tab" id="ttt-st-tab-3" aria-controls="ttt-st-panel-3" aria-selected="false" tabindex="-1" class="-mb-px cursor-pointer border-b-2 border-transparent px-1 py-3 text-sm font-medium text-slate-500 hover:border-slate-300 hover:text-slate-800 aria-selected:border-fuchsia-700 aria-selected:text-fuchsia-800 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-fuchsia-700">Security</button>
    </div>
  </div>
  <div role="tabpanel" id="ttt-st-panel-1" aria-labelledby="ttt-st-tab-1" class="p-5 sm:p-6">
    <form>
      <div class="grid gap-4 sm:grid-cols-2">
        <div><label for="ttt-st-name" class="text-sm font-medium text-slate-700">Display name</label><input id="ttt-st-name" type="text" autocomplete="name" value="Rosa Lindqvist" class="mt-1.5 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2.5 text-[15px] text-slate-900 placeholder:text-slate-400 focus-visible:border-fuchsia-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-fuchsia-700"></div>
        <div><label for="ttt-st-email" class="text-sm font-medium text-slate-700">Email</label><input id="ttt-st-email" type="email" autocomplete="email" value="rosa@example.com" class="mt-1.5 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2.5 text-[15px] text-slate-900 placeholder:text-slate-400 focus-visible:border-fuchsia-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-fuchsia-700"></div>
        <div class="sm:col-span-2"><label for="ttt-st-bio" class="text-sm font-medium text-slate-700">Short bio</label><textarea id="ttt-st-bio" rows="2" placeholder="What you work on" class="mt-1.5 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2.5 text-[15px] text-slate-900 placeholder:text-slate-400 focus-visible:border-fuchsia-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-fuchsia-700"></textarea></div>
      </div>
      <div class="mt-6 flex items-center justify-end gap-4 border-t border-slate-100 pt-4"><p class="ttt-saved text-sm text-emerald-700" role="status"></p><button type="submit" class="cursor-pointer rounded-lg bg-fuchsia-700 px-4 py-2.5 text-sm font-semibold text-white hover:bg-fuchsia-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">Save changes</button></div>
    </form>
  </div>
  <div role="tabpanel" id="ttt-st-panel-2" aria-labelledby="ttt-st-tab-2" hidden class="p-5 sm:p-6">
    <form>
      <div class="divide-y divide-slate-100">
        <label class="flex cursor-pointer items-center justify-between gap-4 py-3"><span><span class="block text-[15px] font-medium text-slate-900">Comments</span><span class="block text-sm text-slate-500">When someone replies to you.</span></span><input type="checkbox" role="switch" class="peer sr-only" checked><span class="relative h-6 w-11 shrink-0 rounded-full bg-slate-300 peer-checked:bg-fuchsia-700 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-fuchsia-700 after:absolute after:top-0.5 after:left-0.5 after:size-5 after:rounded-full after:bg-white after:shadow-xs peer-checked:after:translate-x-5 motion-safe:after:transition-transform" aria-hidden="true"></span></label>
        <label class="flex cursor-pointer items-center justify-between gap-4 py-3"><span><span class="block text-[15px] font-medium text-slate-900">Weekly digest</span><span class="block text-sm text-slate-500">A Monday summary of your projects.</span></span><input type="checkbox" role="switch" class="peer sr-only" checked><span class="relative h-6 w-11 shrink-0 rounded-full bg-slate-300 peer-checked:bg-fuchsia-700 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-fuchsia-700 after:absolute after:top-0.5 after:left-0.5 after:size-5 after:rounded-full after:bg-white after:shadow-xs peer-checked:after:translate-x-5 motion-safe:after:transition-transform" aria-hidden="true"></span></label>
        <label class="flex cursor-pointer items-center justify-between gap-4 py-3"><span><span class="block text-[15px] font-medium text-slate-900">Product news</span><span class="block text-sm text-slate-500">One email a month at most.</span></span><input type="checkbox" role="switch" class="peer sr-only"><span class="relative h-6 w-11 shrink-0 rounded-full bg-slate-300 peer-checked:bg-fuchsia-700 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-fuchsia-700 after:absolute after:top-0.5 after:left-0.5 after:size-5 after:rounded-full after:bg-white after:shadow-xs peer-checked:after:translate-x-5 motion-safe:after:transition-transform" aria-hidden="true"></span></label>
      </div>
      <div class="mt-6 flex items-center justify-end gap-4 border-t border-slate-100 pt-4"><p class="ttt-saved text-sm text-emerald-700" role="status"></p><button type="submit" class="cursor-pointer rounded-lg bg-fuchsia-700 px-4 py-2.5 text-sm font-semibold text-white hover:bg-fuchsia-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">Save changes</button></div>
    </form>
  </div>
  <div role="tabpanel" id="ttt-st-panel-3" aria-labelledby="ttt-st-tab-3" hidden class="p-5 sm:p-6">
    <form>
      <div class="divide-y divide-slate-100">
        <label class="flex cursor-pointer items-center justify-between gap-4 py-3"><span><span class="block text-[15px] font-medium text-slate-900">Two-step sign-in</span><span class="block text-sm text-slate-500">Ask for a code from your authenticator app.</span></span><input type="checkbox" role="switch" class="peer sr-only"><span class="relative h-6 w-11 shrink-0 rounded-full bg-slate-300 peer-checked:bg-fuchsia-700 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-fuchsia-700 after:absolute after:top-0.5 after:left-0.5 after:size-5 after:rounded-full after:bg-white after:shadow-xs peer-checked:after:translate-x-5 motion-safe:after:transition-transform" aria-hidden="true"></span></label>
        <div class="py-3"><p class="text-[15px] font-medium text-slate-900">Active sessions</p><p class="text-sm text-slate-500">Firefox on Linux, Lisbon (this device). Safari on iPhone, Porto, 2 days ago.</p></div>
      </div>
      <div class="mt-6 flex items-center justify-end gap-4 border-t border-slate-100 pt-4"><p class="ttt-saved text-sm text-emerald-700" role="status"></p><button type="submit" class="cursor-pointer rounded-lg bg-fuchsia-700 px-4 py-2.5 text-sm font-semibold text-white hover:bg-fuchsia-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">Save changes</button></div>
    </form>
  </div>
</div>

CSS-only and no-JavaScript alternatives

Two CSS-only tab sets built on radio inputs, and the native exclusive accordion. They work without a script, and each note says what they give up.

CSS-only tabs with radio inputs and named peers

  • peer/ing
  • peer-checked/ing:
  • peer-focus-visible/ing:
  • flex-wrap

No JavaScript: click a label, or focus it and use the arrow keys.

Three hidden radios come first, then the labels, then the panels, all siblings in one flex-wrap row, because a peer only reaches later siblings. Named peers (peer/ing, peer-checked/ing:block) pair each radio with its label and panel, and peer-focus-visible/ing: draws the focus ring on the label. The trade-off: assistive tech hears a radio group, not tabs, the panels are not tied to their controls with tabpanel roles, and Home and End do nothing, so use it where no script is allowed and keep the JavaScript pattern for production.

View code
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-stone-200 sm:p-6">
  <h3 id="ttt-rc-label" class="text-lg font-semibold text-stone-900">Cardamom buns</h3>
  <p class="mt-1 text-sm text-stone-500">12 buns, 3 hours including proofing</p>
  <div role="radiogroup" aria-labelledby="ttt-rc-label" class="mt-3 flex flex-wrap">
    <input type="radio" name="ttt-recipe" id="ttt-rc-ing" class="peer/ing sr-only" checked>
    <input type="radio" name="ttt-recipe" id="ttt-rc-met" class="peer/met sr-only">
    <input type="radio" name="ttt-recipe" id="ttt-rc-sto" class="peer/sto sr-only">
    <label for="ttt-rc-ing" class="relative z-10 -mb-px cursor-pointer border-b-2 border-transparent px-3 py-2.5 text-sm font-medium text-stone-600 hover:text-stone-900 peer-checked/ing:border-orange-600 peer-checked/ing:text-orange-800 peer-focus-visible/ing:outline-2 peer-focus-visible/ing:outline-orange-600">Ingredients</label>
    <label for="ttt-rc-met" class="relative z-10 -mb-px cursor-pointer border-b-2 border-transparent px-3 py-2.5 text-sm font-medium text-stone-600 hover:text-stone-900 peer-checked/met:border-orange-600 peer-checked/met:text-orange-800 peer-focus-visible/met:outline-2 peer-focus-visible/met:outline-orange-600">Method</label>
    <label for="ttt-rc-sto" class="relative z-10 -mb-px cursor-pointer border-b-2 border-transparent px-3 py-2.5 text-sm font-medium text-stone-600 hover:text-stone-900 peer-checked/sto:border-orange-600 peer-checked/sto:text-orange-800 peer-focus-visible/sto:outline-2 peer-focus-visible/sto:outline-orange-600">Storage</label>
    <div class="hidden w-full border-t border-stone-200 pt-4 peer-checked/ing:block">
      <ul class="space-y-1.5 text-[15px] text-stone-700">
        <li>500 g strong flour</li>
        <li>250 ml warm milk, 75 g butter</li>
        <li>2 tsp freshly ground cardamom</li>
        <li>7 g dried yeast, 60 g sugar</li>
      </ul>
    </div>
    <div class="hidden w-full border-t border-stone-200 pt-4 peer-checked/met:block">
      <ol class="list-decimal space-y-1.5 pl-5 text-[15px] text-stone-700">
        <li>Knead everything into a soft dough, then rest for an hour.</li>
        <li>Roll out, spread with cardamom butter, fold and cut strips.</li>
        <li>Twist into knots and proof for 45 minutes.</li>
        <li>Bake at 200&deg;C for 12 minutes.</li>
      </ol>
    </div>
    <div class="hidden w-full border-t border-stone-200 pt-4 peer-checked/sto:block">
      <p class="text-[15px] leading-relaxed text-stone-700">Best on the day. Freeze cooled buns in a bag for up to a month and warm them for 5 minutes at 160&deg;C.</p>
    </div>
  </div>
</div>

CSS-only segmented tabs with has-checked

  • has-checked:
  • has-focus-visible:
  • group-has-[#id:checked]:block
  • fieldset

No JavaScript: pick an option.

Here the radios sit inside their labels, so has-checked:bg-white styles the selected segment and has-focus-visible: draws the keyboard ring without any sibling order rules. The panels can live anywhere in the form: group-has-[#ttt-ship-home:checked]:block reveals one when the form contains that checked radio. Semantically this is a fieldset with a legend and a radio group, which is honest for a choice that also gets submitted, but it is not a tab set.

View code
<form class="group w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <fieldset>
    <legend class="text-lg font-semibold text-slate-900">How do you want your order?</legend>
    <div class="mt-3 flex gap-1 rounded-xl bg-slate-100 p-1">
      <label class="relative flex flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg px-2 py-2.5 text-sm font-medium text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-red-800 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-red-700"><input type="radio" name="ttt-ship" id="ttt-ship-home" value="home" class="sr-only" checked>Delivery</label>
      <label class="relative flex flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg px-2 py-2.5 text-sm font-medium text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-red-800 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-red-700"><input type="radio" name="ttt-ship" id="ttt-ship-store" value="store" class="sr-only">Pickup</label>
      <label class="relative flex flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg px-2 py-2.5 text-sm font-medium text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-red-800 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-red-700"><input type="radio" name="ttt-ship" id="ttt-ship-locker" value="locker" class="sr-only">Locker</label>
    </div>
  </fieldset>
  <div class="mt-4 hidden group-has-[#ttt-ship-home:checked]:block">
    <p class="text-[15px] text-slate-700">Arrives <span class="font-semibold text-slate-900">Thursday, Nov 6</span> to 48 Elm Row. Free over $50.</p>
  </div>
  <div class="mt-4 hidden group-has-[#ttt-ship-store:checked]:block">
    <p class="text-[15px] text-slate-700">Ready in <span class="font-semibold text-slate-900">2 hours</span> at Canal Street store, open until 20:00.</p>
  </div>
  <div class="mt-4 hidden group-has-[#ttt-ship-locker:checked]:block">
    <p class="text-[15px] text-slate-700">Pick up from <span class="font-semibold text-slate-900">Locker 14, Central Station</span> with a code we text you.</p>
  </div>
</form>

Exclusive accordion with details name

  • <details name>
  • group-open:
  • list-none
  • [&::-webkit-details-marker]:hidden

No JavaScript: open a question, and the open one closes.

Giving every details element the same name attribute makes them an exclusive group: opening one closes the others, the one-panel-at-a-time behavior of tabs with no script. The summary hides its marker with list-none and the arbitrary variant [&::-webkit-details-marker]:hidden for Safari, and group-open: turns the plus into a close icon. For long sections or narrow screens this is often the better pattern than tabs, and each heading stays visible.

View code
<div class="w-full max-w-lg rounded-2xl bg-white px-5 py-2 shadow-sm ring-1 ring-slate-200 sm:px-6">
  <details name="ttt-faq" class="group border-b border-slate-200 last:border-0" open>
    <summary class="flex cursor-pointer list-none items-center justify-between gap-4 rounded-md py-4 text-left text-[15px] font-semibold text-slate-900 hover:text-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-600 [&::-webkit-details-marker]:hidden">
      When will my order ship?
      <span class="inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-900 group-open:bg-slate-900 group-open:text-yellow-300"><svg class="size-4 group-open:rotate-45 motion-safe:transition-transform" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10 4v12M4 10h12" stroke-linecap="round"/></svg></span>
    </summary>
    <p class="pb-4 text-[15px] leading-relaxed text-slate-600">Orders placed before 14:00 leave the warehouse the same working day. You get a tracking link by email.</p>
  </details>
  <details name="ttt-faq" class="group border-b border-slate-200 last:border-0">
    <summary class="flex cursor-pointer list-none items-center justify-between gap-4 rounded-md py-4 text-left text-[15px] font-semibold text-slate-900 hover:text-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-600 [&::-webkit-details-marker]:hidden">
      Can I change the delivery address?
      <span class="inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-900 group-open:bg-slate-900 group-open:text-yellow-300"><svg class="size-4 group-open:rotate-45 motion-safe:transition-transform" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10 4v12M4 10h12" stroke-linecap="round"/></svg></span>
    </summary>
    <p class="pb-4 text-[15px] leading-relaxed text-slate-600">Yes, until the parcel is handed to the courier. Reply to your order email with the new address.</p>
  </details>
  <details name="ttt-faq" class="group border-b border-slate-200 last:border-0">
    <summary class="flex cursor-pointer list-none items-center justify-between gap-4 rounded-md py-4 text-left text-[15px] font-semibold text-slate-900 hover:text-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-600 [&::-webkit-details-marker]:hidden">
      How do returns work?
      <span class="inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-900 group-open:bg-slate-900 group-open:text-yellow-300"><svg class="size-4 group-open:rotate-45 motion-safe:transition-transform" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10 4v12M4 10h12" stroke-linecap="round"/></svg></span>
    </summary>
    <p class="pb-4 text-[15px] leading-relaxed text-slate-600">Send items back within 30 days in their original packaging. Returns are free inside the EU.</p>
  </details>
  <details name="ttt-faq" class="group border-b border-slate-200 last:border-0">
    <summary class="flex cursor-pointer list-none items-center justify-between gap-4 rounded-md py-4 text-left text-[15px] font-semibold text-slate-900 hover:text-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-600 [&::-webkit-details-marker]:hidden">
      Do you ship outside Europe?
      <span class="inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-900 group-open:bg-slate-900 group-open:text-yellow-300"><svg class="size-4 group-open:rotate-45 motion-safe:transition-transform" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M10 4v12M4 10h12" stroke-linecap="round"/></svg></span>
    </summary>
    <p class="pb-4 text-[15px] leading-relaxed text-slate-600">We ship to the US, Canada and Australia. Duties are shown at checkout.</p>
  </details>
</div>

What Does a Tailwind Tabs Component Consist Of?

Start with the container, which takes role=”tablist” and is usually a flex row with a bottom border. Each tab inside it is a button with role=”tab” and an id, plus aria-controls pointing at its panel. The panel is a div with role=”tabpanel”, and aria-labelledby points back at the tab.

Tailwind itself ships no script for any of this. The Tailwind CSS framework supplies the classes, and the switching comes from JavaScript, a framework, a headless library or a CSS-only trick.

Put the tab role on buttons, not links. A button takes focus and fires on Enter and Space without extra code, which is why tabs in plain HTML start from button elements.

A base tab carries px-4 py-2 text-sm font-medium plus border-b-2 border-transparent. The transparent border reserves the space, so the layout does not jump when the active tab gets its colored border.

Tailwind Labs’ v4 upgrade guide has a couple of changes that touch this markup. The hidden attribute now beats display utilities like flex or block, so a hidden panel stays hidden even when it carries a display class. And buttons use the browser default cursor now, so you add cursor-pointer to each tab yourself.

How to Build Tailwind Tabs With Vanilla JavaScript

The script is short. It flips aria-selected and the hidden attribute on click and moves focus on arrow keys, while the markup and aria-selected variants handle everything else.

  1. Create the tablist as a div with role=”tablist” and an aria-label, holding one button per tab. Give each button role=”tab”, an id, and aria-controls set to its panel id.
  2. Set the starting state with aria-selected=”true” and tabindex=”0″ on the first tab, and aria-selected=”false” and tabindex=”-1″ on the rest.
  3. Create one div per panel with role=”tabpanel” and aria-labelledby set to its tab id. Add the hidden attribute to every panel except the first.
  4. Style the tabs with aria-selected: variants, such as aria-selected:border-indigo-600 and aria-selected:text-indigo-600, over a border-transparent base.
  5. Write the click handler. Set aria-selected to false and tabindex to -1 on every tab, mark the clicked tab true and 0, add hidden to every panel, then remove it from the matching one.
  6. Add a keydown handler on the tablist for Left and Right Arrow, Home and End. It moves focus and calls the same switch function.

Because the classes never change, a few lines of JavaScript that set attributes are enough.

The one thing that goes wrong is toggling color classes from the click handler. The visual state and aria-selected drift apart the first time someone edits one and forgets the other.

Which Method Should Track the Active Tab?

For a single tab set on a static page, vanilla JavaScript is plenty. If you have several tab sets and Alpine.js is already loaded, use Alpine. React or Vue apps do better with Headless UI or another headless primitive.

CSS-only tabs with peer and radio inputs suit a prototype where no script is allowed, and only where losing the full ARIA and keyboard pattern costs nothing. A data-attribute library such as Flowbite or Preline makes sense when you want a prebuilt look and no custom behavior.

Alpine.js

Alpine keeps the active tab in one variable on x-data. Each button sets it on click, a :class binding styles the active state, and each panel gets x-show.

According to the Alpine docs, x-show toggles display: none, and the class binding accepts an object of class names mapped to boolean expressions.

Alpine adds no roles on the tablist, tabs or panels. You also write the :aria-selected binding on each tab yourself, plus a @keydown handler for the arrow keys. State, show and hide, and class binding are covered. The rest stays your job.

React and Vue with Headless UI

Tailwind Labs released Headless UI v2.0 for React on May 7, 2024.

Tabs use TabGroup, TabList, Tab, TabPanels and TabPanel. Props cover manual activation, vertical layout, defaultIndex, and a controlled selectedIndex with onChange, per the Headless UI docs.

Each tab exposes data-selected, data-hover, data-focus and data-active, so data-selected:bg-blue-500 styles the active tab with no render props.

In a project that already uses Tailwind CSS in React, this is the shortest path to a tab set that handles focus and keys correctly.

Vue is a different story. The Vue link on the React docs page points to the v1 documentation, which describes the older data-headlessui-state attribute and ui-selected plugin variants instead of data-selected.

Check which attribute your installed version emits before copying classes from either page.

CSS-only tabs with peer and radio inputs

Radio inputs hold the state, labels act as the tabs, and peer-checked variants reveal the matching panel. No script runs.

Tailwind’s own docs show the mechanism with named peers: a radio marked peer/draft, a label styled with peer-checked/draft, and a hidden panel that becomes block through peer-checked/draft:block.

Plenty of CSS-only tab examples rely on the same trick, and each radio group needs its own unique name (daisyUI’s tab docs repeat that rule).

The catch, also from the Tailwind docs, is that a peer marker only reaches later siblings. Every radio input must sit before the panels it controls.

Whether a CSS-only build exposes tabpanel roles and aria-controls depends entirely on your markup, so check it against the W3C roles list before shipping.

Data-attribute libraries

Flowbite wants data-tabs-toggle on the tablist, data-tabs-target on each button, role=”tab”, aria-selected, and the Flowbite JavaScript loaded. Active and inactive classes pass through data-tabs-active-classes and data-tabs-inactive-classes.

Preline works from data-hs-tab with aria-controls on each button, started by HSTabs.autoInit(). An eventType option switches between click and hover.

Flowbite’s code is MIT licensed, according to its docs. I’d take either library only if you want its prebuilt look too, because the runtime dependency replaces a job a small script already covers.

How to Make Tailwind Tabs Accessible

The W3C Authoring Practices tabs pattern defines what accessible tabs need. Each tab is tied to its panel, exactly one tab is marked selected, and arrow keys move focus inside the tablist.

The ARIA roles and states tell a screen reader that the buttons form one tab set and which panel each one controls.

The container gets role=”tablist”, each tab gets role=”tab” and each panel gets role=”tabpanel”. Label the tablist with aria-label, or with aria-labelledby when a visible label already exists. Wiring runs both ways, with aria-controls on each tab and aria-labelledby on each panel.

Set aria-selected=”true” on the active tab and false on all the others. Vertical lists also need aria-orientation=”vertical”, since horizontal is the default.

KeyW3C patternHeadless UI
Left / Right ArrowPrevious or next tab, wrapping at the endsSelects previous or next non-disabled tab
Home / EndOptional: first or last tabHome or PageUp, End or PageDown
Enter / SpaceActivates when activation is not automaticActivates only when manual is set
Up / Down ArrowReplace Left and Right when vertical; ignored when horizontalSame, with the vertical prop

The W3C pattern recommends activating a tab on focus when its panel appears without noticeable latency. When panels are slow, automatic activation makes keyboard navigation sluggish, so manual activation fits better.

Headless UI activates on arrow keys by default and offers a manual prop for tabs that trigger expensive work.

Both sources land on the same rule. Activate automatically by default, and go manual when switching costs time.

Panels need one more detail. When a tabpanel holds no focusable element, the W3C note says to give it tabindex=”0″ so the Tab key lands on it from the tablist.

How to Style Active, Hover and Focus States in Tailwind CSS v4

Style the active tab from its own state attribute. That means aria-selected: on hand-built tabs and data-selected: on Headless UI tabs, with focus-visible: handling keyboard focus.

Tailwind’s aria-selected variant targets [aria-selected=”true”], so the script never touches a color class.

Inactive tabs usually get border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700. The active tab adds aria-selected:border-indigo-600 aria-selected:text-indigo-600. For keyboard focus, focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 does the job.

The v4 upgrade guide changed four defaults that tab styles run into.

  • Hover applies only when the primary input supports hover, so touch screens never show it. Tailwind advises treating hover as an enhancement.
  • The outline-2 utility now draws a solid outline by itself, and outline-none was renamed outline-hidden.
  • The bare ring utility dropped from 3px to 1px and from blue-500 to currentColor, so use ring-3 and name the color.
  • Border utilities default to currentColor, which means an underline tab needs an explicit color.

Headless UI reaches the same touch behavior from the other side. Its data-hover attribute is ignored on touch devices, and data-focus acts like focus-visible without false positives from imperative focusing (Tailwind Labs, Headless UI v2.0 announcement, May 7, 2024).

One constraint decides whether v4 is an option at all. Tailwind CSS v4.0 targets Safari 16.4, Chrome 111 and Firefox 128, and projects that support older browsers stay on v3.4, per the upgrade guide.

Dark mode

Pair each color class with a dark: twin. By default the dark variant follows prefers-color-scheme, so tabs match the operating system with no toggle.

PartLightDark
Tablist borderborder-gray-200dark:border-gray-700
Inactive tab texttext-gray-500dark:text-gray-400
Active tabaria-selected:text-indigo-600 aria-selected:border-indigo-600dark:aria-selected:text-indigo-400 dark:aria-selected:border-indigo-400

For a manual toggle, override the variant in CSS with @custom-variant dark (&:where(.dark, .dark *)). Dark classes then apply whenever a dark class sits above the element, and a data-theme attribute works the same way (Tailwind CSS docs).

A toggle that offers light, dark and system reads the stored choice from localStorage and falls back to matchMedia.

Underline, Pill, Vertical, Icon and Badge Tabs

All of these styles share the same markup. Only the class pattern changes: a bottom border for underlines, a rounded fill for pills, flex-col for vertical lists, and extra children inside the button for icons and badges.

Nielsen Norman Group’s tabs guidance (written in 2024) calls for more than one selection indicator on the active tab. Pair the border or fill with a color change instead of relying on a single signal.

Underline tabs

The tablist carries the baseline, with flex gap-6 border-b border-gray-200, and each tab carries a thicker border that overlaps it. On the tab that means -mb-px border-b-2 border-transparent pb-3 text-sm font-medium. The active state is aria-selected:border-indigo-600 aria-selected:text-indigo-600.

Use border-b-2 on the tab, not border-b. The same guidance warns against single-pixel strokes and poor-contrast line colors.

Skip bold on the active tab. A heavier weight widens the label and shifts every neighbor on each click.

Pill tabs

Pills swap the line for a filled shape.

Add gap-1 to the tablist and rounded-md px-3 py-2 text-sm font-medium to each tab. The active pill takes aria-selected:bg-indigo-100 and aria-selected:text-indigo-700, while inactive tabs get hover:bg-gray-100.

The usual failure is weak color contrast on the inactive labels. Nielsen Norman Group cites MongoDB, where faded tab labels read like disabled text.

Vertical tabs

Wrap the tablist and the panels in one flex row with flex gap-6, which turns the tablist into a fixed-width column. The tablist itself takes w-48 shrink-0 flex-col. Each tab gets border-l-2 border-transparent px-4 py-2 text-left, and the active one adds aria-selected:border-indigo-600 aria-selected:bg-gray-50. Panels need min-w-0 flex-1.

Reserve this layout for settings screens, where the list is the main control. Nielsen Norman Group warns that vertical and bottom lists get overlooked, and it cites Okta’s tabs, placed to the right of the panel.

Tabs with icons

Heroicons, the SVG icon set from the makers of Tailwind CSS, fits tabs without extra setup.

Make the button inline-flex items-center gap-2 and size the icon with size-4 shrink-0. Because the SVG icons draw with currentColor, aria-selected:text-indigo-600 on the button recolors icon and label together.

Heroicons added aria-hidden=”true” to its icons in v1.0.5 (project changelog), so the text label carries the tab’s name. An icon-only tab needs an aria-label, and Nielsen Norman Group advises pairing icons with labels anyway.

Badge tabs

Visa’s design system describes number badges on its mobile tab bar. A badge in the top-right corner of the tab icon shows dynamic information, such as the number of messages.

In Tailwind that is a span after the label with rounded-full px-2 py-0.5 text-xs font-medium, a neutral fill when inactive and aria-selected:bg-indigo-100 when active. Put inline-flex items-center gap-2 on the tab so everything stays aligned.

The badge text joins the tab’s name, so a bare number reads as “Inbox 12”. Add visually hidden text with sr-only, such as “unread messages”.

How to Make Tailwind Tabs Responsive

How many tabs you have decides the approach. Short labels can scroll in a row. A longer set is better off with a select swapped in below the sm breakpoint, and an accordion makes sense when each panel is long. Tailwind’s sm breakpoint starts at 40rem (640px), per its responsive design docs.

This is a responsive design decision before it is a styling one, because every option trades discoverability for space.

Tailwind’s mobile-first breakpoints trip people up here. An unprefixed class applies at every width, and sm: means from the small breakpoint upward, so the select takes sm:hidden and the tablist takes hidden sm:flex.

Select fallback

Below sm, show a native select with the same options as the tabs and an sr-only label. From sm up, show the tablist. One change handler on the select and one click handler on the tabs should both call the same switch function.

The weak spot is that a select is a different control, so screen readers announce a select, not a tab set. Both elements also need to stay in sync when the viewport crosses the breakpoint.

Horizontal scrolling

Add overflow-x-auto and whitespace-nowrap to the tablist so the row scrolls instead of wrapping.

The scrollbar-none utility, documented in the current Tailwind v4 docs, hides the scrollbar while the row still scrolls. It also hides the only cue that more tabs exist.

Nielsen Norman Group makes the same point about overflowing tab lists. Hidden tabs become less discoverable and cost more interaction. It shows Patagonia’s store, where a button scrolled the tab list on narrow viewports.

Keep the last visible tab partly cut off, or scroll the active tab into view when it changes.

Accordion on phones

Nielsen Norman Group finds that accordions work better than tabs on mobile because screen space is limited. It adds that tabs can suit desktop better, since a closed accordion can leave the page looking empty.

If each panel runs longer than a screenful, skip the select and render the sections as an accordion below sm.

Switching orientation at a breakpoint

A sidebar-style list that stacks above the panel on narrow screens takes flex-col md:flex-row on the wrapper and flex-row md:flex-col on the tablist.

The CSS flips, but aria-orientation and the arrow-key mapping do not. The W3C pattern ties aria-orientation to the visual layout, so update it with a matchMedia listener.

When the tab set sits in a narrow card or sidebar, size it by its parent instead. Mark the parent @container and use variants like @md:flex-row, which respond to container width (Tailwind docs).

Where to Get Ready-Made Tailwind Tabs

One tab set doesn’t justify a library, so build it by hand. Past that, it depends on the stack. React has Headless UI or Radix UI Tabs, Vue has Headless UI, and plain HTML projects can use daisyUI, Flowbite or Preline UI.

Tailwind Plus is no longer sold to new customers.

SourceBehavior comes fromCost typeCatch
Headless UIReact and Vue componentsFree, MIT (npm)Unstyled, so every class is yours
daisyUIPlugin classes, optional radio inputsFree, open sourceClass names changed in v5
FlowbiteData attributes plus its JavaScriptFree core, paid ProPro blocks sold separately
Preline UI@preline/tabs pluginMIT plus Fair Use terms, paid ProRedistribution limits
Tailwind Plusel-tab-group web componentPaid, one-timeClosed to new customers

Tailwind Labs’ own announcement of September 9, 2026 says the company is joining Shopify. It closes sign-ups for new Tailwind Plus customers and lets existing customers keep access.

Several outlets call the move an acquisition, a word the announcement avoids.

Tailwind Plus Elements, which supplies the el-tab-group component, needs a commercial license, per the license file in its npm package. Without an existing license, that rules it out.

Older tutorials still send readers there. The React tabs page on headlessui.com, checked in October 2026, still points to Tailwind Plus for styled examples.

daisyUI v5 renamed several classes, per its upgrade guide. tabs-bordered became tabs-border, tabs-lifted became tabs-lift and tabs-boxed became tabs-box, so snippets written for v4 lose their variant styling.

Preline’s open-source library is free for unlimited projects, and redistribution is allowed under the MIT and Fair Use licenses as long as attribution and license notices stay in place (Preline pricing page). Pro is a lifetime license for a single developer, and it prohibits redistribution, public sharing and use in derivative themes or generators.

shadcn/ui’s Tabs page now offers Base UI (the default), React Aria and Radix UI variants. The Radix variant sends you to the Radix Tabs docs for its API. Radix marks triggers with data-state=”active”, so data-[state=active]: classes style the active tab.

When Tailwind Tabs Are the Wrong Pattern

Tabs are a poor fit when the choices are really pages, when readers need to compare panels side by side, or when the row passes six or seven tabs. They also go wrong when a panel holds state that has to survive a switch.

If the choices are pages, you don’t want tabs. Nielsen Norman Group separates in-page tabs, which swap related content without leaving the page, from navigation tabs, which load other pages. It calls Behance’s “Creatives for Hire” tab a link masquerading as a tab, because it opened a different site.

A row of links between pages is site navigation. Build it with anchors in a nav element and mark the current page with aria-current=”page”, not role=”tab”.

Readers who need two panels at once get a worse deal. Tabs tax short-term memory when information under different tabs must be compared, according to Nielsen Norman Group, which cites Google’s ad settings page as the cautionary example. Put that content on one page with subheadings.

Overflowing rows are the next problem, and design systems that set a limit land in the same place.

SourceLimitWording
IBM Carbon (v10 docs)No more than six tabs in most scenariosGuideline, not a hard maximum
Zuora OccamFewer than seven“Usually” rule of thumb
Nielsen Norman GroupAs few as possibleOverflow turns the bar into a carousel

Carbon’s current v11 guidance no longer states the six-tab figure. It lets line and contained tabs scroll when they overflow and sets its own limits for grid-aware tabs.

Past that point, an accordion built with Tailwind holds long lists of sections better, since its labels can run longer (Nielsen Norman Group).

When one panel matters most, tabs bury it. The default tab gets the attention, and Nielsen Norman Group advises keeping content in the other tabs supplemental rather than critical.

Live state is the last trap. Headless UI’s TabPanel unmounts hidden panels by default, and Radix UI Tabs mounts only the selected content unless forceMount is set. A half-filled form inside a hidden tab is gone when the user switches back, unless the form state lives above the tab set.

Vue projects on Headless UI have their own wrinkle. As of October 2026, npm lists @headlessui/vue at 1.7.23, while the React package moved on to v2. The two do not share a feature set.

Styling hooks differ between the two packages too. The Vue docs page documents data-headlessui-state with ui-selected plugin variants, so test which attribute your installed version emits.

Tailwind Tabs FAQ

How do you open a specific tab from a link or keep it after a reload?

Read the tab id from location.hash on load and pass it to the same switch function the click handler uses. Write the hash back with history.replaceState on each switch, so the Back button does not fill with entries.

In Headless UI, drive selectedIndex and onChange from a query parameter.

Can the panel change fade in with Tailwind classes?

Yes, but the hidden attribute removes the panel with display: none, which blocks an ordinary transition. Tailwind’s starting: variant sets the opening style for an element that goes from display: none to visible, so transition-opacity duration-300 starting:opacity-0 produces a fade.

Add motion-reduce:transition-none for users who ask for less motion.

How do you disable a tab?

On hand-built tabs, set aria-disabled=”true” and style it with the aria-disabled: variant, for example aria-disabled:opacity-50 and aria-disabled:cursor-not-allowed. The click and arrow-key handlers then skip that tab.

Headless UI takes a disabled prop on Tab and skips disabled tabs during keyboard movement, per its docs.

How do you make tabs fill the full width?

Make the tablist flex and give each tab flex-1 text-center, so the tabs share the row in equal widths.

Use sm:flex-1 instead if you want content-sized tabs on phones.

Is it fine to nest one tab set inside another?

Yes, as long as each set has its own ids and its own keydown handler. Put the inner tablist inside an outer tabpanel, and give CSS-only radio groups unique names.

Stop at two levels, because the inner labels start to compete with the outer ones.

How to Test a Tab Set Before Shipping

Test with the keyboard first, a screen reader second, and a narrow screen last.

The order follows dependency. A tab set that traps focus or skips a tab fails the screen reader pass before it starts, and the narrow-screen pass only checks layout.

For the keyboard pass, tab into the tablist, then arrow through every tab. With the screen reader, confirm each tab announces its name and selected state. Then resize across the sm breakpoint and repeat the arrow keys.

Hand-built markup carries a cost, because the team owns the keydown handler after launch.

Verified October 29, 2026. Reopened Tailwind Plus sign-ups, or a v2 release of Headless UI for Vue, would change which source to start from.