32 CSS Tab Designs16 / 32

Pure CSSMIT licensed

Halo Tabs

Mobile-style nav-bar tabs in a soft white pill. Inactive icons are line-only grey. Activating a tab tints the icon mint, blooms a soft pink halo behind it, and slides the label in to the right of the icon — no underline, just the color shift and the label appearance.

Published Updated

Live Demo
Try it

The code

<div class="tt04">
  <nav class="tt04n">
    <input type="radio" name="tt04" id="tt04-r1" checked />
    <label class="tt04b" for="tt04-r1">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M3 11l9-8 9 8v10a2 2 0 0 1-2 2h-4v-7H9v7H5a2 2 0 0 1-2-2z" />
      </svg>
      <span class="tt04l">Home</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r2" />
    <label class="tt04b" for="tt04-r2">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M5 8l7-5 7 5v3H5z" />
        <path d="M9 11h6v8H9z" />
        <path d="M3 19h18" />
      </svg>
      <span class="tt04l">Strategy</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r3" />
    <label class="tt04b" for="tt04-r3">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M7 3h10v4l-3 5 3 5v4H7v-4l3-5-3-5z" />
        <line x1="9" y1="3" x2="9" y2="3" />
      </svg>
      <span class="tt04l">Period</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r4" />
    <label class="tt04b" for="tt04-r4">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6z" />
        <polyline points="9 12 11 14 15 10" />
      </svg>
      <span class="tt04l">Security</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r5" />
    <label class="tt04b" for="tt04-r5">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <circle cx="12" cy="12" r="3" />
        <path
          d="M19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.5 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.3 2.6a7 7 0 0 0-2.1 1.2l-2.5-1-2 3.4 2 1.6A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.6 2 3.4 2.5-1a7 7 0 0 0 2.1 1.2L10 21h4l.3-2.6a7 7 0 0 0 2.1-1.2l2.5 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2z"
        />
      </svg>
      <span class="tt04l">Settings</span>
    </label>
  </nav>
</div>
.tt04 {
  background: #f0f1f1;
  padding: 0;
  font-family: "Nunito", ui-sans-serif, system-ui, sans-serif;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.tt04n {
  position: relative;
  display: flex;
  gap: 4px;
  padding: 18px 22px;
  background: #fefefe;
  border-radius: 999px;
  margin: 14px;
  flex: 1;
  max-width: 480px;
  box-shadow: 0 6px 22px -8px rgba(0, 0, 0, 0.12);
  font-size: 1em;
}

.tt04n input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.tt04b {
  position: relative;
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 6px 4px;
  border: 0;
  background: transparent;
  cursor: pointer;
  overflow: hidden;
  color: #d5dadd;
  transition: flex 0.45s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s;
}

.tt04b:hover {
  color: #9aa0a4;
}
/* Soft pink halo behind the active icon, top-left offset like the reference */

.tt04halo {
  position: absolute;
  top: 14%;
  left: 22%;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fde4ec;
  transform: scale(0);
  transform-origin: center;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 0;
  pointer-events: none;
}

.tt04i {
  width: 26px;
  height: 26px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.tt04l {
  position: relative;
  z-index: 2;
  font: 800 15px/1 "Nunito", ui-sans-serif, system-ui;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: #2dd4bf;
  /* Hidden until active — slides in to the right of the icon */
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  transform: translateX(-8px);
  transition: max-width 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s 0.1s, transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

.tt04n input:checked + .tt04b {
  flex: 2.6;
  color: #2dd4bf;
}

.tt04n input:checked + .tt04b .tt04halo {
  transform: scale(1);
}

.tt04n input:checked + .tt04b .tt04l {
  max-width: 200px;
  opacity: 1;
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .tt04halo,
    .tt04l,
    .tt04b {
    transition: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Tab Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Halo Tabs
Source: https://codefronts.com/navigation/css-tabs/halo-tabs/

Mobile-style nav-bar tabs in a soft white pill. Inactive icons are line-only grey. Activating a tab tints the icon mint, blooms a soft pink halo behind it, and slides the label in to the right of the icon — no underline, just the color shift and the label appearance.
## HTML
```html
<div class="tt04">
  <nav class="tt04n">
    <input type="radio" name="tt04" id="tt04-r1" checked />
    <label class="tt04b" for="tt04-r1">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M3 11l9-8 9 8v10a2 2 0 0 1-2 2h-4v-7H9v7H5a2 2 0 0 1-2-2z" />
      </svg>
      <span class="tt04l">Home</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r2" />
    <label class="tt04b" for="tt04-r2">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M5 8l7-5 7 5v3H5z" />
        <path d="M9 11h6v8H9z" />
        <path d="M3 19h18" />
      </svg>
      <span class="tt04l">Strategy</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r3" />
    <label class="tt04b" for="tt04-r3">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M7 3h10v4l-3 5 3 5v4H7v-4l3-5-3-5z" />
        <line x1="9" y1="3" x2="9" y2="3" />
      </svg>
      <span class="tt04l">Period</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r4" />
    <label class="tt04b" for="tt04-r4">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6z" />
        <polyline points="9 12 11 14 15 10" />
      </svg>
      <span class="tt04l">Security</span>
    </label>
    <input type="radio" name="tt04" id="tt04-r5" />
    <label class="tt04b" for="tt04-r5">
      <span class="tt04halo" aria-hidden="true"> </span>
      <svg class="tt04i" viewBox="0 0 24 24" aria-hidden="true">
        <circle cx="12" cy="12" r="3" />
        <path
          d="M19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.5 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.3 2.6a7 7 0 0 0-2.1 1.2l-2.5-1-2 3.4 2 1.6A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.6 2 3.4 2.5-1a7 7 0 0 0 2.1 1.2L10 21h4l.3-2.6a7 7 0 0 0 2.1-1.2l2.5 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2z"
        />
      </svg>
      <span class="tt04l">Settings</span>
    </label>
  </nav>
</div>
```
## CSS
```css
.tt04 {
  background: #f0f1f1;
  padding: 0;
  font-family: "Nunito", ui-sans-serif, system-ui, sans-serif;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.tt04n {
  position: relative;
  display: flex;
  gap: 4px;
  padding: 18px 22px;
  background: #fefefe;
  border-radius: 999px;
  margin: 14px;
  flex: 1;
  max-width: 480px;
  box-shadow: 0 6px 22px -8px rgba(0, 0, 0, 0.12);
  font-size: 1em;
}

.tt04n input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.tt04b {
  position: relative;
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 6px 4px;
  border: 0;
  background: transparent;
  cursor: pointer;
  overflow: hidden;
  color: #d5dadd;
  transition: flex 0.45s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s;
}

.tt04b:hover {
  color: #9aa0a4;
}
/* Soft pink halo behind the active icon, top-left offset like the reference */

.tt04halo {
  position: absolute;
  top: 14%;
  left: 22%;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fde4ec;
  transform: scale(0);
  transform-origin: center;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 0;
  pointer-events: none;
}

.tt04i {
  width: 26px;
  height: 26px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.tt04l {
  position: relative;
  z-index: 2;
  font: 800 15px/1 "Nunito", ui-sans-serif, system-ui;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: #2dd4bf;
  /* Hidden until active — slides in to the right of the icon */
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  transform: translateX(-8px);
  transition: max-width 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s 0.1s, transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

.tt04n input:checked + .tt04b {
  flex: 2.6;
  color: #2dd4bf;
}

.tt04n input:checked + .tt04b .tt04halo {
  transform: scale(1);
}

.tt04n input:checked + .tt04b .tt04l {
  max-width: 200px;
  opacity: 1;
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .tt04halo,
    .tt04l,
    .tt04b {
    transition: none;
  }
}
```

How this works

Halo Tabs is a hand-coded radio-group nav wrapped in a pill-shaped white bar (.tt04n at #fefefe, border-radius: 999px) sitting on a soft #f0f1f1 stage. Each label (.tt04b) is flex: 1 1 0 so all five slots share the width evenly until one is chosen. The typography is Nunito at 800 15px/1 with a 0.01em letter-spacing, giving the pill a boutique cosmetics register rather than a system-UI look.

The active state is driven purely by the sibling combinator: .tt04n input:checked + .tt04b flips flex from 1 to 2.6, so the chosen tab absorbs the free width and the others compress. Inside that same rule, color shifts from #d5dadd to #2dd4bf (mint), the .tt04halo pseudo-badge scales from 0 to 1, and the .tt04l label unfolds from max-width: 0 to 200px while translating from -8px to 0. Every transition uses cubic-bezier(0.4, 0, 0.2, 1) so the halo, the label slide, and the flex reflow arrive together.

The halo itself is a 22px round #fde4ec panel offset to top 14%, left 22% and layered at z-index: 0, so the stroked SVG icon at z-index: 2 sits crisply on top. The markup is semantic (radio inputs + labels), keyboard-navigable through arrow keys, and copy-paste ready with no JavaScript, no dependencies, and a scoped .tt04 parent so the pill does not collide with your host styles. A prefers-reduced-motion: reduce block strips all three transitions for INP-safe accessibility.

Make it yours

  • Swap the mint accent #2dd4bf and pink halo #fde4ec for a brand pair — a taupe base with a rose-gold halo reads luxury; a navy base with a lemon halo reads sportwear. Update both .tt04b checked color and .tt04halo background.
  • Retune the width absorption by editing the flex: 2.6 value on the checked label. Values near 1.8 keep inactive icons legible on narrow bars; values above 3.2 give the active tab a hero read at the cost of squeezed neighbours.
  • Resize the halo through its width, height, and top/left percentages. A 28px halo offset to top: 8% creates a fuller aura for larger 32px icons; a 16px halo tight to the glyph reads as a subtle backdrop.
  • Change the label transition delay by adjusting the opacity 0.3s 0.1s compound. Raising the delay to 0.2s makes the label fade in only after the flex expansion settles, giving a two-beat reveal rather than a synchronous bloom.
  • Replace Nunito with a variable font like Inter or Manrope and drop the weight to 700 for a more editorial pill. Keep the letter-spacing: 0.01em — it prevents the bold weight from feeling cramped against the icon.

Gotchas — read before shipping

  • The halo is absolutely positioned with hard-coded percentages (top: 14%, left: 22%). If you widen .tt04b padding or change the icon size, the halo drifts off the glyph — re-derive both offsets or switch to inset: 0 with margin: auto and a fixed width/height.
  • Radio inputs are visually hidden via opacity: 0 and pointer-events: none, but they are still in the tab order. Confirm your focus ring lives on .tt04n input:focus-visible + .tt04b or you will ship an invisible keyboard focus state and fail WCAG 2.1 focus-visible.
  • The SVG icons carry aria-hidden="true", which means the label text is the only accessible name. If you replace .tt04l with an icon-only tab, add an aria-label to the label element or screen readers will announce nothing.
  • The overflow: hidden on .tt04b is what hides the collapsed label — do not remove it while debugging or the hidden label will spill sideways and force horizontal scroll on the pill.
  • The pill uses max-width: 480px and flex: 1 on the nav. On very narrow viewports the SVGs stay 26px but the padding shrinks; test at 320px and either drop icon size to 22px or widen the outer pill padding so labels do not touch the pill edge on activation.
  • The transition on .tt04b animates the flex shorthand. Some older builds of Safari would ignore this — the source is SSR-safe and framework-agnostic, but if you need Safari 14 support, animate flex-grow explicitly instead of the shorthand.

Browser support

ChromeSafariFirefoxEdge
88+14+78+88+

The :checked + sibling combinator, cubic-bezier timing, and flex transitions are stable everywhere. On Safari below 14, animate flex-grow directly for reliable width interpolation.

Techniques used in this demo

Search CodeFronts

Loading…