CSS Tabs Generator

Next

Tabs are everywhere: settings panes, documentation sites, product galleries. Native HTML has no dedicated tab element, so developers roll their own. This generator builds a simple tab bar in HTML + CSS: an accent colour for the active tab, a corner radius for the buttons, and a hover state. Paste the markup into your project and wire up the tab switching with your own JavaScript.

How to build tabs

  1. 1

    Pick an accent colour

    The background and text colour of the active tab. Choose a colour that matches your brand.

  2. 2

    Set the corner radius

    Rounds the top corners of the tab buttons. Higher values give a softer look.

  3. 3

    Edit the labels

    The output starts with three buttons, "Overview", "Details" and "Settings": rename them to match your content.

  4. 4

    Copy the code

    The tab markup comes with its own <style> block. Paste both into your page.

  5. 5

    Add tab switching

    The generator produces the visual part only. Hook the buttons up to your own JavaScript or UI library to show and hide the panels.

Correct accessible markup

<div class="tabs">
  <div role="tablist" aria-label="Product info">
    <button role="tab" id="tab-1" aria-selected="true"  aria-controls="panel-1" tabindex="0">Overview</button>
    <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Specs</button>
    <button role="tab" id="tab-3" aria-selected="false" aria-controls="panel-3" tabindex="-1">Reviews</button>
  </div>
  <section role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</section>
  <section role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>...</section>
  <section role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>...</section>
</div>

Critical attributes:

  • role="tablist" wraps the buttons.
  • role="tab" on each button, with aria-selected and aria-controls.
  • tabindex uses the roving tabindex pattern: only the active tab is 0, others are -1. Arrow keys move focus between them.
  • role="tabpanel" on each panel, with aria-labelledby pointing to its tab.
  • hidden on inactive panels (not display: none alone, the attribute is what AT uses).

Keyboard contract

Key Action
Tab Move into / out of the tablist
ArrowLeft Previous tab (wraps to last)
ArrowRight Next tab (wraps to first)
Home First tab
End Last tab
Enter / Space Activate focused tab (if manual activation)

Automatic activation (tab switches as focus moves) vs. manual (focus moves, tab activates on Enter), WAI-ARIA allows both. Automatic is easier for sighted users; manual is better when tab content is heavy to render.

Underlined style

[role="tablist"] {
  display: flex;
  border-bottom: 1px solid #e5e7eb;
}
[role="tab"] {
  padding: 0.75rem 1rem;
  background: transparent;
  border: 0;
  color: #6b7280;
  position: relative;
  cursor: pointer;
}
[role="tab"][aria-selected="true"] {
  color: #111827;
}
[role="tab"][aria-selected="true"]::after {
  content: "";
  position: absolute;
  left: 1rem; right: 1rem; bottom: -1px;
  height: 2px;
  background: #2563eb;
}

Pilled style

[role="tab"] {
  padding: 0.5rem 0.9rem;
  border-radius: 9999px;
  border: 0;
  background: transparent;
}
[role="tab"][aria-selected="true"] {
  background: #2563eb;
  color: #fff;
}

Focus visible

Always draw a focus ring when focus arrives via keyboard:

[role="tab"]:focus-visible {
  outline: 2px solid #60a5fa;
  outline-offset: 2px;
}

The JavaScript

Roughly:

const tabs = document.querySelectorAll('[role="tab"]');
tabs.forEach(tab => {
  tab.addEventListener('click', () => activate(tab));
  tab.addEventListener('keydown', e => {
    const i = [...tabs].indexOf(tab);
    if (e.key === 'ArrowRight') tabs[(i + 1) % tabs.length].focus();
    if (e.key === 'ArrowLeft')  tabs[(i - 1 + tabs.length) % tabs.length].focus();
    if (e.key === 'Home') tabs[0].focus();
    if (e.key === 'End')  tabs[tabs.length - 1].focus();
  });
});

function activate(tab) {
  tabs.forEach(t => {
    const panel = document.getElementById(t.getAttribute('aria-controls'));
    const active = t === tab;
    t.setAttribute('aria-selected', active);
    t.setAttribute('tabindex', active ? '0' : '-1');
    panel.hidden = !active;
  });
  tab.focus();
}

Thirty lines total, zero dependencies.

Tabs vs. accordions

On narrow screens, tab rows get cramped. Common pattern: tabs on desktop, accordion on mobile. Use a container query or media query to switch visual treatment without changing the ARIA roles.

Frequently Asked Questions

For accessible tabs, yes. Without role="tablist" and role="tab", screen readers treat the buttons as regular buttons and users lose the navigation convention. The alternative is a plain <details> / <summary> accordion, which has built-in semantics.

Automatic (tab activates as focus moves) is fine when each tab’s content is cheap to render. Manual (focus moves, Enter activates) is better when switching tabs triggers heavy work like fetching or rendering a large form.

For content-bearing tabs (documentation, product tabs), yes, use a hash (#specs) or query param so links can deep-link to a specific tab and the active tab persists across reloads. For UI tabs (settings panes) the URL is optional.

Only with the checkbox/radio hack, which lacks arrow-key navigation and ARIA states. For a genuinely accessible implementation, the small JS snippet is worth it. If you need server-rendered fallbacks, link each tab to a unique URL and render the corresponding panel server-side.

Related Tools

Tool available in other languages