32 CSS Tab Designs25 / 32
Pill Nav
Mobile-style bottom-nav with hot-pink active pill that grows around the active icon and label. Inactive items show only icons; activation adds the label inside the pill.
Published Updated
The code
<div class="tt13">
<nav class="tt13n">
<input type="radio" name="tt13" id="tt13-r1" checked />
<label class="tt13b" for="tt13-r1">
<svg class="tt13i" 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="tt13l">Home</span>
</label>
<input type="radio" name="tt13" id="tt13-r2" />
<label class="tt13b" for="tt13-r2">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16" y2="16" />
</svg>
<span class="tt13l">Search</span>
</label>
<input type="radio" name="tt13" id="tt13-r3" />
<label class="tt13b" for="tt13-r3">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 8h-3V5a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v3H4l1 12h14z" />
</svg>
<span class="tt13l">Saved</span>
</label>
<input type="radio" name="tt13" id="tt13-r4" />
<label class="tt13b" for="tt13-r4">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="8" r="4" />
<path d="M3 21v-2a7 7 0 0 1 7-7h4a7 7 0 0 1 7 7v2" />
</svg>
<span class="tt13l">Profile</span>
</label>
</nav>
</div><div class="tt13">
<nav class="tt13n">
<input type="radio" name="tt13" id="tt13-r1" checked />
<label class="tt13b" for="tt13-r1">
<svg class="tt13i" 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="tt13l">Home</span>
</label>
<input type="radio" name="tt13" id="tt13-r2" />
<label class="tt13b" for="tt13-r2">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16" y2="16" />
</svg>
<span class="tt13l">Search</span>
</label>
<input type="radio" name="tt13" id="tt13-r3" />
<label class="tt13b" for="tt13-r3">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 8h-3V5a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v3H4l1 12h14z" />
</svg>
<span class="tt13l">Saved</span>
</label>
<input type="radio" name="tt13" id="tt13-r4" />
<label class="tt13b" for="tt13-r4">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="8" r="4" />
<path d="M3 21v-2a7 7 0 0 1 7-7h4a7 7 0 0 1 7 7v2" />
</svg>
<span class="tt13l">Profile</span>
</label>
</nav>
</div>.tt13 {
background: #1f2326;
min-height: 100vh;
display: flex;
align-items: center;
padding: 28px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt13n {
flex: 1;
display: flex;
gap: 6px;
padding: 6px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 999px;
}
.tt13n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt13b {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 14px;
border: 0;
background: transparent;
font: 700 12px/1 ui-sans-serif, system-ui;
letter-spacing: 0.04em;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
border-radius: 999px;
flex: 1;
transition: flex 0.45s cubic-bezier(0.34, 1.4, 0.5, 1), background 0.25s, color 0.25s;
}
.tt13b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt13i {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.4s cubic-bezier(0.34, 1.4, 0.5, 1);
}
.tt13l {
max-width: 0;
white-space: nowrap;
overflow: hidden;
transition: max-width 0.45s cubic-bezier(0.65, 0, 0.35, 1) 0.05s;
}
.tt13n input:checked + .tt13b {
background: #ec4899;
color: #1a1a1a;
flex: 2.4;
}
.tt13n input:checked + .tt13b .tt13i {
transform: scale(1.1);
}
.tt13n input:checked + .tt13b .tt13l {
max-width: 120px;
}.tt13 {
background: #1f2326;
min-height: 100vh;
display: flex;
align-items: center;
padding: 28px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt13n {
flex: 1;
display: flex;
gap: 6px;
padding: 6px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 999px;
}
.tt13n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt13b {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 14px;
border: 0;
background: transparent;
font: 700 12px/1 ui-sans-serif, system-ui;
letter-spacing: 0.04em;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
border-radius: 999px;
flex: 1;
transition: flex 0.45s cubic-bezier(0.34, 1.4, 0.5, 1), background 0.25s, color 0.25s;
}
.tt13b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt13i {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.4s cubic-bezier(0.34, 1.4, 0.5, 1);
}
.tt13l {
max-width: 0;
white-space: nowrap;
overflow: hidden;
transition: max-width 0.45s cubic-bezier(0.65, 0, 0.35, 1) 0.05s;
}
.tt13n input:checked + .tt13b {
background: #ec4899;
color: #1a1a1a;
flex: 2.4;
}
.tt13n input:checked + .tt13b .tt13i {
transform: scale(1.1);
}
.tt13n input:checked + .tt13b .tt13l {
max-width: 120px;
}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: Pill Nav
Source: https://codefronts.com/navigation/css-tabs/pill-nav/
Mobile-style bottom-nav with hot-pink active pill that grows around the active icon and label. Inactive items show only icons; activation adds the label inside the pill.
## HTML
```html
<div class="tt13">
<nav class="tt13n">
<input type="radio" name="tt13" id="tt13-r1" checked />
<label class="tt13b" for="tt13-r1">
<svg class="tt13i" 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="tt13l">Home</span>
</label>
<input type="radio" name="tt13" id="tt13-r2" />
<label class="tt13b" for="tt13-r2">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16" y2="16" />
</svg>
<span class="tt13l">Search</span>
</label>
<input type="radio" name="tt13" id="tt13-r3" />
<label class="tt13b" for="tt13-r3">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 8h-3V5a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v3H4l1 12h14z" />
</svg>
<span class="tt13l">Saved</span>
</label>
<input type="radio" name="tt13" id="tt13-r4" />
<label class="tt13b" for="tt13-r4">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="8" r="4" />
<path d="M3 21v-2a7 7 0 0 1 7-7h4a7 7 0 0 1 7 7v2" />
</svg>
<span class="tt13l">Profile</span>
</label>
</nav>
</div>
```
## CSS
```css
.tt13 {
background: #1f2326;
min-height: 100vh;
display: flex;
align-items: center;
padding: 28px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt13n {
flex: 1;
display: flex;
gap: 6px;
padding: 6px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 999px;
}
.tt13n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt13b {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 14px;
border: 0;
background: transparent;
font: 700 12px/1 ui-sans-serif, system-ui;
letter-spacing: 0.04em;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
border-radius: 999px;
flex: 1;
transition: flex 0.45s cubic-bezier(0.34, 1.4, 0.5, 1), background 0.25s, color 0.25s;
}
.tt13b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt13i {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.4s cubic-bezier(0.34, 1.4, 0.5, 1);
}
.tt13l {
max-width: 0;
white-space: nowrap;
overflow: hidden;
transition: max-width 0.45s cubic-bezier(0.65, 0, 0.35, 1) 0.05s;
}
.tt13n input:checked + .tt13b {
background: #ec4899;
color: #1a1a1a;
flex: 2.4;
}
.tt13n input:checked + .tt13b .tt13i {
transform: scale(1.1);
}
.tt13n input:checked + .tt13b .tt13l {
max-width: 120px;
}
```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: Pill Nav
Source: https://codefronts.com/navigation/css-tabs/pill-nav/
Mobile-style bottom-nav with hot-pink active pill that grows around the active icon and label. Inactive items show only icons; activation adds the label inside the pill.
## HTML
```html
<div class="tt13">
<nav class="tt13n">
<input type="radio" name="tt13" id="tt13-r1" checked />
<label class="tt13b" for="tt13-r1">
<svg class="tt13i" 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="tt13l">Home</span>
</label>
<input type="radio" name="tt13" id="tt13-r2" />
<label class="tt13b" for="tt13-r2">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16" y2="16" />
</svg>
<span class="tt13l">Search</span>
</label>
<input type="radio" name="tt13" id="tt13-r3" />
<label class="tt13b" for="tt13-r3">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 8h-3V5a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v3H4l1 12h14z" />
</svg>
<span class="tt13l">Saved</span>
</label>
<input type="radio" name="tt13" id="tt13-r4" />
<label class="tt13b" for="tt13-r4">
<svg class="tt13i" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="8" r="4" />
<path d="M3 21v-2a7 7 0 0 1 7-7h4a7 7 0 0 1 7 7v2" />
</svg>
<span class="tt13l">Profile</span>
</label>
</nav>
</div>
```
## CSS
```css
.tt13 {
background: #1f2326;
min-height: 100vh;
display: flex;
align-items: center;
padding: 28px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt13n {
flex: 1;
display: flex;
gap: 6px;
padding: 6px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 999px;
}
.tt13n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt13b {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 14px;
border: 0;
background: transparent;
font: 700 12px/1 ui-sans-serif, system-ui;
letter-spacing: 0.04em;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
border-radius: 999px;
flex: 1;
transition: flex 0.45s cubic-bezier(0.34, 1.4, 0.5, 1), background 0.25s, color 0.25s;
}
.tt13b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt13i {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.4s cubic-bezier(0.34, 1.4, 0.5, 1);
}
.tt13l {
max-width: 0;
white-space: nowrap;
overflow: hidden;
transition: max-width 0.45s cubic-bezier(0.65, 0, 0.35, 1) 0.05s;
}
.tt13n input:checked + .tt13b {
background: #ec4899;
color: #1a1a1a;
flex: 2.4;
}
.tt13n input:checked + .tt13b .tt13i {
transform: scale(1.1);
}
.tt13n input:checked + .tt13b .tt13l {
max-width: 120px;
}
```How this works
The .tt13 demo is a bottom-nav pill in the Notion and Linear filter idiom: a charcoal #1f2326 ground carries a translucent rgba(255,255,255,0.04) track with a border-radius:999px capsule, and the active label sits inside a hot-pink #ec4899 pill. Inactive items show only the SVG icon; activation both grows the pill and reveals the text label from a width-clipped inline element. It is the same asymmetric behaviour you see across product filters, where the active state is the only one that needs a text handle.
The pill growth is a flex-basis animation. Every .tt13b starts at flex:1, so all four labels share the track equally. On input:checked, the active one jumps to flex:2.4 and the other three redistribute the remaining space, all easing on cubic-bezier(0.34, 1.4, 0.5, 1) across 0.45s. The text label uses a max-width trick: .tt13l is max-width:0; overflow:hidden; white-space:nowrap by default, and the checked state expands to max-width:120px with a 0.05s delay so the pill has started growing before the letters appear.
The SVG icons are stroked, not filled, at 1.7 stroke-width with rounded joins — matching the pill's rounded ends. On check, the icon nudges to transform:scale(1.1). Semantic radios, keyboard operable, no dependencies, no JavaScript, SSR-safe, and framework-agnostic. INP stays flat because every animated property is either transform, opacity, or a compositor-friendly flex-basis.
Make it yours
- Swap the pink to any brand accent by editing #ec4899 in input:checked + .tt13b. Confirm the pill text colour #1a1a1a still passes WCAG 2.1 AA — the current pair lands around 8:1, so darker pinks are safe but pastels will fail.
- Tune the pill-expansion ratio via the flex:2.4 value. Smaller like flex:1.8 keeps the four items closer to equal; larger like flex:3 makes the active pill dominate. The max-width:120px on .tt13l caps how much label can appear regardless.
- The track is rgba(255,255,255,0.04) with a rgba(255,255,255,0.06) border. On lighter grounds, flip both to rgba(0,0,0,0.04) and rgba(0,0,0,0.06). The border-radius:999px keeps the capsule regardless of height.
- Icons are inline SVG with stroke:currentColor, so they inherit the label colour. Replace the four paths with your own 24x24 viewBox glyphs. Keep stroke-linecap:round and stroke-linejoin:round so they match the pill's rounded ends.
- The label reveal delay is 0.05s after the flex transition starts. If you slow the pill down past 0.6s, push the delay to around 0.1s so the text does not race the pill; leave both defaults for the standard filter register.
Gotchas — read before shipping
- Animating flex is compositor-friendly here because the parent is a plain flex row with no wrapping. Add flex-wrap:wrap and you invite layout thrash on every check — INP will jump on lower-end Android. Keep the nav single-row.
- The label uses max-width not width. Switching to width:0 collapses the flex item entirely and the pill loses its label reserve; max-width lets the intrinsic content size the pill while the transition drives visibility.
- SVG icons need aria-hidden="true" because the label text carries the semantics. Remove it and screen readers announce a nameless graphic before the label, doubling the utterance on WCAG 2.1 audits.
- The nav is position:relative only implicitly through its flex parent. If you drop the outer .tt13 centring and let the nav float free, the pill still animates but the whole capsule can wander vertically inside a taller container — pin with align-self if you nest it.
- The hot-pink pill against dark grey text lands with strong contrast, but the inactive label rgba(232,226,208,0.55) against #1f2326 hovers near the WCAG 2.1 AA large-text threshold. Bump the alpha to 0.7 if your audit needs headroom for body-size labels.
- The .tt13n input rule uses position:absolute. That absolute positioning is relative to the nearest positioned ancestor — the nav is not positioned, so the inputs escape to the viewport. This is fine because they are opacity-zero, but do not add position:relative to .tt13n without also confining input z-index.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 88+ | 14+ | 94+ | 88+ |
No :has() needed — the pill nav routes purely through adjacent-sibling selectors. Flex-basis transitions are baseline; the demo runs on older browsers than the rest of the cluster.