Open demo

CSS code

HTML
<div class="itabs">
  <div role="tablist" aria-label="Mailbox">
    <button type="button" role="tab" id="it-tab-0" aria-selected="true" aria-controls="it-panel-0"><span class="ic" aria-hidden="true">📥</span>Inbox<span class="bd" aria-hidden="true">12</span><span class="sr-only">, 12 unread</span></button>
    <button type="button" role="tab" id="it-tab-1" aria-selected="false" aria-controls="it-panel-1" tabindex="-1"><span class="ic" aria-hidden="true">⭐️</span>Starred</button>
    <button type="button" role="tab" id="it-tab-2" aria-selected="false" aria-controls="it-panel-2" tabindex="-1"><span class="ic" aria-hidden="true">📤</span>Sent</button>
    <button type="button" role="tab" id="it-tab-3" aria-selected="false" aria-controls="it-panel-3" tabindex="-1"><span class="ic" aria-hidden="true">🗑️</span>Trash<span class="bd" aria-hidden="true">3</span><span class="sr-only">, 3 items</span></button>
  </div>
  <div role="tabpanel" id="it-panel-0" aria-labelledby="it-tab-0" tabindex="0"><p>Icon-over-label tabs with unread badges — the mail-app pattern. Badges are absolutely positioned pills, so counts up to three digits fit without shifting the layout.</p></div>
  <div role="tabpanel" id="it-panel-1" aria-labelledby="it-tab-1" tabindex="0" hidden><p>Nothing starred yet. The active state combines three cues: color, a bottom bar, and a soft background — redundancy that keeps it readable for everyone.</p></div>
  <div role="tabpanel" id="it-panel-2" aria-labelledby="it-tab-2" tabindex="0" hidden><p>Sent mail would list here. Tabs flex equally, so the row survives two tabs or six.</p></div>
  <div role="tabpanel" id="it-panel-3" aria-labelledby="it-tab-3" tabindex="0" hidden><p>Three items in the trash. Emptying it is left as an exercise for your backend.</p></div>
</div>
CSS
/* Icon tabs with badges: equal-width tabs with an icon over the label and an absolutely positioned count badge. */
.itabs{--it-accent:#6d28d9;--it-active-bg:#f8f5ff;--it-hover-bg:#faf9fd;--it-muted:#6b6488;--it-badge:#e11d48;
  box-sizing:border-box;width:100%;max-width:560px;background:#fff;border:1px solid #e9e5f4;border-radius:16px;overflow:hidden;
  box-shadow:0 16px 36px -22px rgba(40,25,90,.2);color:#211c33}
.itabs [role=tablist]{display:flex;border-bottom:1px solid #efecf7}
.itabs [role=tab]{position:relative;flex:1;display:flex;flex-direction:column;align-items:center;gap:5px;margin:0;border:0;border-bottom:3px solid transparent;
  background:none;padding:16px 8px 13px;font:inherit;font-size:.78rem;font-weight:700;color:var(--it-muted);cursor:pointer;
  transition:background-color .15s,color .15s,border-color .15s}
.itabs [role=tab]:hover{background:var(--it-hover-bg)}
.itabs [role=tab][aria-selected=true]{color:var(--it-accent);border-bottom-color:var(--it-accent);background:var(--it-active-bg)}
.itabs .ic{font-size:1.25rem}
.itabs .bd{position:absolute;top:10px;right:calc(50% - 26px);background:var(--it-badge);color:#fff;font-size:.6rem;font-weight:900;
  min-width:17px;height:17px;border-radius:99px;display:flex;align-items:center;justify-content:center;padding:0 4px}
.itabs .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.itabs [role=tabpanel]{padding:24px 28px;min-height:110px}
.itabs [role=tabpanel] p{margin:0;font-size:.88rem;line-height:1.7;color:#5f5878}
.itabs [role=tab]:focus-visible,.itabs [role=tabpanel]:focus-visible{outline:2px solid var(--it-accent);outline-offset:-4px}
.itabs [role=tabpanel]:focus-visible{border-radius:0 0 16px 16px}
.itabs [hidden]{display:none}
@media (prefers-reduced-motion:reduce){.itabs [role=tab]{transition:none}}
JavaScript
/* WAI-ARIA tabs: click or use the arrow keys to select, Home/End jump to the first/last tab.
   Only the selected tab is in the Tab order (roving tabindex). */
document.querySelectorAll('.itabs').forEach(root => {
  const list = root.querySelector('[role=tablist]');
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = (tab, focus) => {
    tabs.forEach(t => {
      const on = t === tab;
      t.setAttribute('aria-selected', on);
      t.tabIndex = on ? 0 : -1;
      document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
    });
    if (focus) tab.focus();
  };
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t) select(t); });
  list.addEventListener('keydown', e => {
    const i = tabs.indexOf(e.target), n = tabs.length;
    const j = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
    if (i < 0 || j === undefined) return;
    e.preventDefault();
    select(tabs[j % n], true);
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="itabs">
  <ul class="nav nav-justified" role="tablist" aria-label="Mailbox">
    <li class="nav-item" role="presentation"><button class="nav-link active" type="button" id="it-tab-0" data-bs-toggle="tab" data-bs-target="#it-panel-0" role="tab" aria-controls="it-panel-0" aria-selected="true"><span class="ic" aria-hidden="true">📥</span>Inbox<span class="badge rounded-pill" aria-hidden="true">12</span><span class="visually-hidden">, 12 unread</span></button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="it-tab-1" data-bs-toggle="tab" data-bs-target="#it-panel-1" role="tab" aria-controls="it-panel-1" aria-selected="false" tabindex="-1"><span class="ic" aria-hidden="true">⭐️</span>Starred</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="it-tab-2" data-bs-toggle="tab" data-bs-target="#it-panel-2" role="tab" aria-controls="it-panel-2" aria-selected="false" tabindex="-1"><span class="ic" aria-hidden="true">📤</span>Sent</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="it-tab-3" data-bs-toggle="tab" data-bs-target="#it-panel-3" role="tab" aria-controls="it-panel-3" aria-selected="false" tabindex="-1"><span class="ic" aria-hidden="true">🗑️</span>Trash<span class="badge rounded-pill" aria-hidden="true">3</span><span class="visually-hidden">, 3 items</span></button></li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane active" id="it-panel-0" role="tabpanel" aria-labelledby="it-tab-0" tabindex="0"><p>Icon-over-label tabs with unread badges — the mail-app pattern. Badges are absolutely positioned pills, so counts up to three digits fit without shifting the layout.</p></div>
    <div class="tab-pane" id="it-panel-1" role="tabpanel" aria-labelledby="it-tab-1" tabindex="0"><p>Nothing starred yet. The active state combines three cues: color, a bottom bar, and a soft background — redundancy that keeps it readable for everyone.</p></div>
    <div class="tab-pane" id="it-panel-2" role="tabpanel" aria-labelledby="it-tab-2" tabindex="0"><p>Sent mail would list here. Tabs flex equally, so the row survives two tabs or six.</p></div>
    <div class="tab-pane" id="it-panel-3" role="tabpanel" aria-labelledby="it-tab-3" tabindex="0"><p>Three items in the trash. Emptying it is left as an exercise for your backend.</p></div>
  </div>
</div>
CSS
/* Bootstrap .nav-justified with the Tab plugin and .badge counts, restyled through the --bs-nav-link-* and --bs-badge-* variables. */
.itabs{--it-accent:#6d28d9;--it-active-bg:#f8f5ff;--it-hover-bg:#faf9fd;--it-muted:#6b6488;--it-badge:#e11d48;
  width:100%;max-width:560px;background:#fff;border:1px solid #e9e5f4;border-radius:16px;overflow:hidden;
  box-shadow:0 16px 36px -22px rgba(40,25,90,.2);color:#211c33}
.itabs .nav{--bs-nav-link-padding-x:8px;--bs-nav-link-padding-y:16px;--bs-nav-link-font-size:.78rem;--bs-nav-link-font-weight:700;
  --bs-nav-link-color:var(--it-muted);--bs-nav-link-hover-color:var(--it-muted);
  flex-wrap:nowrap;border-bottom:1px solid #efecf7}
.itabs .nav-link{position:relative;display:flex;flex-direction:column;align-items:center;gap:5px;padding-bottom:13px;line-height:normal;
  border-bottom:3px solid transparent;border-radius:0;transition:background-color .15s,color .15s,border-color .15s}
.itabs .nav-link:hover{background:var(--it-hover-bg)}
.itabs .nav-link.active{color:var(--it-accent);border-bottom-color:var(--it-accent);background:var(--it-active-bg)}
.itabs .ic{font-size:1.25rem}
.itabs .badge{--bs-badge-padding-x:4px;--bs-badge-padding-y:0;--bs-badge-font-size:.6rem;--bs-badge-font-weight:900;--bs-badge-color:#fff;
  position:absolute;top:10px;right:calc(50% - 26px);min-width:17px;height:17px;display:flex;align-items:center;justify-content:center;background:var(--it-badge)}
.itabs .tab-pane{padding:24px 28px;min-height:110px}
.itabs .tab-pane p{margin:0;font-size:.88rem;line-height:1.7;color:#5f5878}
.itabs .nav-link:focus-visible,.itabs .tab-pane:focus-visible{box-shadow:none;outline:2px solid var(--it-accent);outline-offset:-4px}
.itabs .tab-pane:focus-visible{border-radius:0 0 16px 16px}
@media (prefers-reduced-motion:reduce){.itabs .nav-link{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="itabs w-full max-w-[560px] overflow-hidden rounded-2xl border border-[#e9e5f4] bg-white text-[#211c33] shadow-[0_16px_36px_-22px_rgba(40,25,90,.2)]">
  <div role="tablist" aria-label="Mailbox" class="flex border-b border-[#efecf7]">
    <button type="button" role="tab" id="it-tab-0" aria-selected="true" aria-controls="it-panel-0" class="relative flex flex-1 cursor-pointer flex-col items-center gap-[5px] border-b-3 border-transparent px-2 pt-4 pb-[13px] text-[.78rem] font-bold text-it-muted transition-colors duration-150 hover:bg-it-hover-bg focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent aria-selected:border-it-accent aria-selected:bg-it-active-bg aria-selected:text-it-accent motion-reduce:transition-none"><span class="text-[1.25rem]" aria-hidden="true">📥</span>Inbox<span class="absolute top-2.5 right-[calc(50%-26px)] flex h-[17px] min-w-[17px] items-center justify-center rounded-full bg-it-badge px-1 text-[.6rem] font-black text-white" aria-hidden="true">12</span><span class="sr-only">, 12 unread</span></button>
    <button type="button" role="tab" id="it-tab-1" aria-selected="false" aria-controls="it-panel-1" tabindex="-1" class="relative flex flex-1 cursor-pointer flex-col items-center gap-[5px] border-b-3 border-transparent px-2 pt-4 pb-[13px] text-[.78rem] font-bold text-it-muted transition-colors duration-150 hover:bg-it-hover-bg focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent aria-selected:border-it-accent aria-selected:bg-it-active-bg aria-selected:text-it-accent motion-reduce:transition-none"><span class="text-[1.25rem]" aria-hidden="true">⭐️</span>Starred</button>
    <button type="button" role="tab" id="it-tab-2" aria-selected="false" aria-controls="it-panel-2" tabindex="-1" class="relative flex flex-1 cursor-pointer flex-col items-center gap-[5px] border-b-3 border-transparent px-2 pt-4 pb-[13px] text-[.78rem] font-bold text-it-muted transition-colors duration-150 hover:bg-it-hover-bg focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent aria-selected:border-it-accent aria-selected:bg-it-active-bg aria-selected:text-it-accent motion-reduce:transition-none"><span class="text-[1.25rem]" aria-hidden="true">📤</span>Sent</button>
    <button type="button" role="tab" id="it-tab-3" aria-selected="false" aria-controls="it-panel-3" tabindex="-1" class="relative flex flex-1 cursor-pointer flex-col items-center gap-[5px] border-b-3 border-transparent px-2 pt-4 pb-[13px] text-[.78rem] font-bold text-it-muted transition-colors duration-150 hover:bg-it-hover-bg focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent aria-selected:border-it-accent aria-selected:bg-it-active-bg aria-selected:text-it-accent motion-reduce:transition-none"><span class="text-[1.25rem]" aria-hidden="true">🗑️</span>Trash<span class="absolute top-2.5 right-[calc(50%-26px)] flex h-[17px] min-w-[17px] items-center justify-center rounded-full bg-it-badge px-1 text-[.6rem] font-black text-white" aria-hidden="true">3</span><span class="sr-only">, 3 items</span></button>
  </div>
  <div role="tabpanel" id="it-panel-0" aria-labelledby="it-tab-0" tabindex="0" class="min-h-[110px] px-7 py-6 focus-visible:rounded-b-2xl focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent"><p class="text-[.88rem] leading-[1.7] text-[#5f5878]">Icon-over-label tabs with unread badges — the mail-app pattern. Badges are absolutely positioned pills, so counts up to three digits fit without shifting the layout.</p></div>
  <div role="tabpanel" id="it-panel-1" aria-labelledby="it-tab-1" tabindex="0" hidden class="min-h-[110px] px-7 py-6 focus-visible:rounded-b-2xl focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent"><p class="text-[.88rem] leading-[1.7] text-[#5f5878]">Nothing starred yet. The active state combines three cues: color, a bottom bar, and a soft background — redundancy that keeps it readable for everyone.</p></div>
  <div role="tabpanel" id="it-panel-2" aria-labelledby="it-tab-2" tabindex="0" hidden class="min-h-[110px] px-7 py-6 focus-visible:rounded-b-2xl focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent"><p class="text-[.88rem] leading-[1.7] text-[#5f5878]">Sent mail would list here. Tabs flex equally, so the row survives two tabs or six.</p></div>
  <div role="tabpanel" id="it-panel-3" aria-labelledby="it-tab-3" tabindex="0" hidden class="min-h-[110px] px-7 py-6 focus-visible:rounded-b-2xl focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-it-accent"><p class="text-[.88rem] leading-[1.7] text-[#5f5878]">Three items in the trash. Emptying it is left as an exercise for your backend.</p></div>
</div>
CSS
@theme {
  --color-it-accent: #6d28d9;
  --color-it-active-bg: #f8f5ff;
  --color-it-hover-bg: #faf9fd;
  --color-it-muted: #6b6488;
  --color-it-badge: #e11d48;
}
JavaScript
/* WAI-ARIA tabs: click or use the arrow keys to select, Home/End jump to the first/last tab.
   Only the selected tab is in the Tab order (roving tabindex). */
document.querySelectorAll('.itabs').forEach(root => {
  const list = root.querySelector('[role=tablist]');
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = (tab, focus) => {
    tabs.forEach(t => {
      const on = t === tab;
      t.setAttribute('aria-selected', on);
      t.tabIndex = on ? 0 : -1;
      document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
    });
    if (focus) tab.focus();
  };
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t) select(t); });
  list.addEventListener('keydown', e => {
    const i = tabs.indexOf(e.target), n = tabs.length;
    const j = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
    if (i < 0 || j === undefined) return;
    e.preventDefault();
    select(tabs[j % n], true);
  });
});

About this set

Four equal-width tabs in a mail-app layout, Inbox, Starred, Sent and Trash, each with an emoji icon above its label and red count badges on Inbox and Trash. The selected tab gets three cues at once: purple text, a purple bar along its bottom edge and a pale purple background, so it stays clear without relying on colour alone. Use it for inboxes, notification centres and mobile-style navigation inside a card, where the icons help scanning and the counts show what needs attention. The badges are absolutely positioned pills, so a count of one digit or three does not shift the labels. The tabs share the width equally, so the row works with two tabs or six. Each badge number is hidden from screen readers and repeated in visually hidden text such as 12 unread, so the tab is announced as a sentence rather than Inbox12. The Bootstrap version uses .nav-justified with the Tab plugin and .badge pills, and the Tailwind version uses flex-1 tabs and aria-selected utilities.

What’s included

  • Icon over label with equal-width tabs
  • Count badges that never shift the layout
  • Selected state uses colour, a bottom bar and a background
  • Badge counts announced as words, not run into the label
  • Muted labels darkened to meet 4.5:1

Accessibility

The tabs sit in a tablist labelled Mailbox, the emoji are aria-hidden, and each badge number is aria-hidden with a visually hidden phrase such as 12 unread in its place. Only the selected tab is in the Tab order; Left and Right (or Up and Down) move between tabs and select them, and Home and End jump to the first and last tab. Focus draws a 2px purple outline inside the tab or panel, and transitions are removed under prefers-reduced-motion.

Customise it

Set --it-accent, --it-active-bg, --it-hover-bg, --it-muted and --it-badge on .itabs in plain CSS and Bootstrap, where the badge also reads the --bs-badge-* variables. In Tailwind edit the --color-it-* theme colours.