Free Component

Pill Tabs

A segmented tab control with a sliding active pill and proper ARIA roles. Arrow keys move between tabs, as assistive technology users expect.

  • light
  • tabs
  • navigation
  • javascript
  • accessible

The code

Plain HTML, CSS and JavaScript with no dependencies. Copy a single file, or choose "Full file" for a complete page you can save as index.html and open in a browser.

<div class="tabs">
  <div class="tablist" role="tablist" aria-label="Pricing period">
    <button role="tab" aria-selected="true" aria-controls="panel-0" id="tab-0">Monthly</button>
    <button role="tab" aria-selected="false" aria-controls="panel-1" id="tab-1" tabindex="-1">Yearly</button>
    <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">Lifetime</button>
    <span class="indicator" aria-hidden="true"></span>
  </div>
  <div class="panel" role="tabpanel" id="panel-0" aria-labelledby="tab-0">&#8377;999 billed every month.</div>
  <div class="panel" role="tabpanel" id="panel-1" aria-labelledby="tab-1" hidden>&#8377;9,990 billed yearly. Two months free.</div>
  <div class="panel" role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>&#8377;24,999 once. Yours forever.</div>
</div>

The AI prompt

Paste this into an AI coding tool to generate the same component, then ask it to adapt the colours, text or framework to your project.

Works with Claude · ChatGPT · Cursor · v0 · Bolt · Gemini
Create an accessible segmented "pill tabs" component with a sliding indicator, using HTML, CSS and vanilla JavaScript.

Markup:
- A container with role="tablist" holding three <button role="tab"> elements (Monthly, Yearly, Lifetime) with aria-selected, aria-controls and ids; inactive tabs have tabindex="-1".
- Three role="tabpanel" divs linked with aria-labelledby; inactive panels use the hidden attribute. Show prices in Indian rupees.
- An empty span.indicator inside the tablist.

Styling:
- Tablist: a 3-column grid, fully rounded, light grey #e9ebf3, 5px padding.
- Buttons are transparent with grey bold text; the selected one has white text.
- The indicator is absolutely positioned, one third of the inner width, indigo #2506ad, and moves with transform: translateX(calc(var(--index, 0) * 100%)) using a 0.3s cubic-bezier transition.
- Panels are white cards with a 1px border and 16px radius.

Behaviour:
- Clicking a tab selects it: update aria-selected and tabindex on all tabs, toggle hidden on panels, and set the --index CSS variable on the tablist.
- Left and Right arrow keys move to the previous/next tab, wrapping around, and move focus.

Return a single HTML file with <style> and <script> tags.

How to build it, step by step

  1. 1

    Mark up the tabs with the ARIA tab pattern: role='tablist', role='tab' and role='tabpanel', connected with aria-controls and aria-labelledby.

  2. 2

    Lay the buttons out in an equal-column grid and add an absolutely positioned indicator behind them.

  3. 3

    Move the indicator with translateX(index x 100%). Because it is exactly one column wide, each step lands on the next tab.

  4. 4

    Store the active index in a CSS variable so JavaScript only needs to set one value.

  5. 5

    On selection, update aria-selected, tabindex and the hidden attribute so the visual state and the accessible state always match.

  6. 6

    Handle the arrow keys: in the tab pattern, Tab moves in and out of the tablist while arrows move between tabs.

Customisation tips

  • For a different number of tabs, change the grid column count and the divisor in the indicator width.
  • In React, keep the index in useState and pass it as style={{ '--index': index }}.
  • Use this as a monthly/yearly switch on the Pricing Table section.

More components