32 CSS Tab Designs17 / 32
Two Weights
Pure type-weight contrast. Inactive sits at 200 hairline, active jumps to 900 black with a subtle scale lift. The brand violet underlines the active label.
Published Updated
The code
<div class="tt05">
<nav class="tt05n">
<input type="radio" name="tt05" id="tt05-r1" checked />
<label class="tt05b" for="tt05-r1">Overview</label>
<input type="radio" name="tt05" id="tt05-r2" />
<label class="tt05b" for="tt05-r2">Activity</label>
<input type="radio" name="tt05" id="tt05-r3" />
<label class="tt05b" for="tt05-r3">Settings</label>
</nav>
<div class="tt05p">A summary of the project, members, and recent decisions.</div>
<div class="tt05p">Edits, comments, and team mentions over the last seven days.</div>
<div class="tt05p">Permissions, integrations, and billing controls.</div>
</div><div class="tt05">
<nav class="tt05n">
<input type="radio" name="tt05" id="tt05-r1" checked />
<label class="tt05b" for="tt05-r1">Overview</label>
<input type="radio" name="tt05" id="tt05-r2" />
<label class="tt05b" for="tt05-r2">Activity</label>
<input type="radio" name="tt05" id="tt05-r3" />
<label class="tt05b" for="tt05-r3">Settings</label>
</nav>
<div class="tt05p">A summary of the project, members, and recent decisions.</div>
<div class="tt05p">Edits, comments, and team mentions over the last seven days.</div>
<div class="tt05p">Permissions, integrations, and billing controls.</div>
</div>.tt05 {
min-height: 100vh;
background: #15151d;
padding: 26px 22px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt05n {
display: flex;
gap: 28px;
}
.tt05n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt05b {
position: relative;
border: 0;
background: transparent;
padding: 0 0 8px;
font: 200 26px/1 ui-sans-serif, system-ui;
color: #e8e2d0;
cursor: pointer;
letter-spacing: -0.02em;
font-variation-settings: "wght" 200;
transform: scale(0.96);
transform-origin: bottom left;
transition: font-weight 0.45s, font-variation-settings 0.45s, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt05b::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: #7c5cff;
transform-origin: left;
transform: scaleX(0);
transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt05b:hover {
font-variation-settings: "wght" 400;
}
.tt05n input:checked + .tt05b {
font-weight: 900;
font-variation-settings: "wght" 900;
transform: scale(1);
}
.tt05n input:checked + .tt05b::after {
transform: scaleX(1);
}
.tt05p {
display: none;
padding-top: 16px;
font: 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
max-width: 360px;
}
.tt05:has(#tt05-r1:checked) .tt05p:nth-of-type(1),
.tt05:has(#tt05-r2:checked) .tt05p:nth-of-type(2),
.tt05:has(#tt05-r3:checked) .tt05p:nth-of-type(3) {
display: block;
}.tt05 {
min-height: 100vh;
background: #15151d;
padding: 26px 22px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt05n {
display: flex;
gap: 28px;
}
.tt05n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt05b {
position: relative;
border: 0;
background: transparent;
padding: 0 0 8px;
font: 200 26px/1 ui-sans-serif, system-ui;
color: #e8e2d0;
cursor: pointer;
letter-spacing: -0.02em;
font-variation-settings: "wght" 200;
transform: scale(0.96);
transform-origin: bottom left;
transition: font-weight 0.45s, font-variation-settings 0.45s, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt05b::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: #7c5cff;
transform-origin: left;
transform: scaleX(0);
transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt05b:hover {
font-variation-settings: "wght" 400;
}
.tt05n input:checked + .tt05b {
font-weight: 900;
font-variation-settings: "wght" 900;
transform: scale(1);
}
.tt05n input:checked + .tt05b::after {
transform: scaleX(1);
}
.tt05p {
display: none;
padding-top: 16px;
font: 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
max-width: 360px;
}
.tt05:has(#tt05-r1:checked) .tt05p:nth-of-type(1),
.tt05:has(#tt05-r2:checked) .tt05p:nth-of-type(2),
.tt05:has(#tt05-r3:checked) .tt05p:nth-of-type(3) {
display: block;
}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: Two Weights
Source: https://codefronts.com/navigation/css-tabs/two-weights/
Pure type-weight contrast. Inactive sits at 200 hairline, active jumps to 900 black with a subtle scale lift. The brand violet underlines the active label.
## HTML
```html
<div class="tt05">
<nav class="tt05n">
<input type="radio" name="tt05" id="tt05-r1" checked />
<label class="tt05b" for="tt05-r1">Overview</label>
<input type="radio" name="tt05" id="tt05-r2" />
<label class="tt05b" for="tt05-r2">Activity</label>
<input type="radio" name="tt05" id="tt05-r3" />
<label class="tt05b" for="tt05-r3">Settings</label>
</nav>
<div class="tt05p">A summary of the project, members, and recent decisions.</div>
<div class="tt05p">Edits, comments, and team mentions over the last seven days.</div>
<div class="tt05p">Permissions, integrations, and billing controls.</div>
</div>
```
## CSS
```css
.tt05 {
min-height: 100vh;
background: #15151d;
padding: 26px 22px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt05n {
display: flex;
gap: 28px;
}
.tt05n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt05b {
position: relative;
border: 0;
background: transparent;
padding: 0 0 8px;
font: 200 26px/1 ui-sans-serif, system-ui;
color: #e8e2d0;
cursor: pointer;
letter-spacing: -0.02em;
font-variation-settings: "wght" 200;
transform: scale(0.96);
transform-origin: bottom left;
transition: font-weight 0.45s, font-variation-settings 0.45s, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt05b::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: #7c5cff;
transform-origin: left;
transform: scaleX(0);
transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt05b:hover {
font-variation-settings: "wght" 400;
}
.tt05n input:checked + .tt05b {
font-weight: 900;
font-variation-settings: "wght" 900;
transform: scale(1);
}
.tt05n input:checked + .tt05b::after {
transform: scaleX(1);
}
.tt05p {
display: none;
padding-top: 16px;
font: 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
max-width: 360px;
}
.tt05:has(#tt05-r1:checked) .tt05p:nth-of-type(1),
.tt05:has(#tt05-r2:checked) .tt05p:nth-of-type(2),
.tt05:has(#tt05-r3:checked) .tt05p:nth-of-type(3) {
display: block;
}
```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: Two Weights
Source: https://codefronts.com/navigation/css-tabs/two-weights/
Pure type-weight contrast. Inactive sits at 200 hairline, active jumps to 900 black with a subtle scale lift. The brand violet underlines the active label.
## HTML
```html
<div class="tt05">
<nav class="tt05n">
<input type="radio" name="tt05" id="tt05-r1" checked />
<label class="tt05b" for="tt05-r1">Overview</label>
<input type="radio" name="tt05" id="tt05-r2" />
<label class="tt05b" for="tt05-r2">Activity</label>
<input type="radio" name="tt05" id="tt05-r3" />
<label class="tt05b" for="tt05-r3">Settings</label>
</nav>
<div class="tt05p">A summary of the project, members, and recent decisions.</div>
<div class="tt05p">Edits, comments, and team mentions over the last seven days.</div>
<div class="tt05p">Permissions, integrations, and billing controls.</div>
</div>
```
## CSS
```css
.tt05 {
min-height: 100vh;
background: #15151d;
padding: 26px 22px 22px;
font-family: ui-sans-serif, system-ui, sans-serif;
width: 100%;
}
.tt05n {
display: flex;
gap: 28px;
}
.tt05n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt05b {
position: relative;
border: 0;
background: transparent;
padding: 0 0 8px;
font: 200 26px/1 ui-sans-serif, system-ui;
color: #e8e2d0;
cursor: pointer;
letter-spacing: -0.02em;
font-variation-settings: "wght" 200;
transform: scale(0.96);
transform-origin: bottom left;
transition: font-weight 0.45s, font-variation-settings 0.45s, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt05b::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: #7c5cff;
transform-origin: left;
transform: scaleX(0);
transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt05b:hover {
font-variation-settings: "wght" 400;
}
.tt05n input:checked + .tt05b {
font-weight: 900;
font-variation-settings: "wght" 900;
transform: scale(1);
}
.tt05n input:checked + .tt05b::after {
transform: scaleX(1);
}
.tt05p {
display: none;
padding-top: 16px;
font: 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
max-width: 360px;
}
.tt05:has(#tt05-r1:checked) .tt05p:nth-of-type(1),
.tt05:has(#tt05-r2:checked) .tt05p:nth-of-type(2),
.tt05:has(#tt05-r3:checked) .tt05p:nth-of-type(3) {
display: block;
}
```How this works
Two Weights is a type-foundry demo built on one idea: no color shift, no motion — the active tab announces itself by jumping from a 200 hairline to a 900 black weight. The .tt05 stage sits on a near-ink #15151d ground with a warm bone #e8e2d0 label color, echoing a printed specimen page. The nav is a plain flex row (gap: 28px) of radio inputs paired with .tt05b labels sized at 26px/1 with a tight letter-spacing: -0.02em.
The weight change is dual-tracked for maximum coverage. Static font-weight: 900 handles legacy or non-variable font stacks, and font-variation-settings: "wght" 900 handles variable fonts smoothly. Both are declared on .tt05n input:checked + .tt05b, and both animate over 0.45s — modern variable fonts interpolate the axis in real time, giving a specimen-style dial from hairline to black. Alongside the weight jump, the active tab lifts from scale(0.96) to scale(1) using cubic-bezier(0.34, 1.56, 0.64, 1) for a small overshoot, and a 2px violet #7c5cff underline scaleXes in from left origin.
Panel switching is a pure CSS :has() selector: .tt05:has(#tt05-r1:checked) .tt05p:nth-of-type(1) flips its matching panel to display: block. Panels use ui-serif at 12px/1.6 at 70% opacity to read as annotation body copy against the display-weight labels. The whole component is semantic HTML, keyboard-navigable through native radio arrow keys, copy-paste with no dependencies, framework-agnostic, and SSR-safe. Hover previews the interaction with an intermediate wght 400 setting, so users get a live weight taste before commitment.
Make it yours
- Swap the violet underline #7c5cff for a foundry accent — a hot cyan like #00d1ff reads futurist; an ink-red like #c43a32 pulls the demo toward pressroom register. Only .tt05b::after background needs editing.
- Change the weight endpoints. Setting inactive to 300 and active to 800 gives a softer specimen with legible inactive labels; going 100 to 900 maxes out the drama but requires a truly variable font like Inter Variable or Roboto Flex.
- Retune the overshoot with cubic-bezier(0.34, 1.56, 0.64, 1). Lowering the 1.56 to 1.2 flattens the bounce for a serious tone; raising to 1.8 pushes into playful territory that reads better in editorial magazine demos than SaaS UIs.
- Replace the ui-sans stack with a display face — Inter Variable for neutral, Fraunces for warm serif, or Recursive for a mono-adjacent specimen. Confirm the loaded face carries the wght axis or the animation will fall back to a discrete jump.
- The panels use a fixed max-width: 360px — widen to 560px for longer editorial captions or drop to 240px for tight column layouts. The ui-serif body stack keeps the annotation register even at wider measures.
Gotchas — read before shipping
- The font-weight transition only interpolates on browsers that support smooth weight animation and only on variable fonts. On static-weight stacks the label will still snap between 200 and 900 — the 0.45s duration is graceful degradation, not a guarantee.
- Because .tt05b jumps from scale 0.96 to 1, and the ::after underline is at bottom: 0, the underline visually shifts up a hair on activation. If you need pixel-locked underlines, pin the underline to .tt05n instead of the label.
- The :has() panel switching requires Safari 15.4+, Chrome 105+, and Firefox 121+. On older Firefox the panels will all stay hidden — either progressively enhance with a JS fallback or ship a @supports not selector(:has(*)) block that shows the first panel unconditionally.
- The radios are absolutely positioned with opacity: 0. They remain focusable, but the visible focus state lives on .tt05b. Add .tt05n input:focus-visible + .tt05b with an outline before shipping, or WCAG 2.1 focus-visible fails silently.
- The active label scales from 0.96. Because transform-origin: bottom left, tab widths visibly shift during activation. On very narrow viewports this can push later tabs off-screen — set the nav to flex-wrap: wrap or reduce the active scale to 0.99 if reflow bothers you.
- There is no prefers-reduced-motion guard on the source as written. For a strict accessibility ship, add a @media (prefers-reduced-motion: reduce) block that zeroes the transition on .tt05b and the ::after — the weight will still swap, just instantly.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 105+ | 15.4+ | 121+ | 105+ |
The :has() panel switcher sets the floor. Variable-font weight interpolation is stable in all three; on older Firefox use a JS fallback for the panel visibility.