32 CSS Tab Designs17 / 32

Pure CSSMIT licensed

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

Live Demo
Try it

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>
.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;
}
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: 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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Search CodeFronts

Loading…