32 CSS Tab Designs22 / 32

Pure CSSMIT licensed

Bauhaus Block

Each tab is a distinct primary-color block. Active tab rises 6px and casts a hard offset shadow. Color and geometry as the entire indicator.

Published Updated

Live Demo
Try it

The code

<div class="tt10">
  <nav class="tt10n">
    <input type="radio" name="tt10" id="tt10-r1" checked />
    <label class="tt10b tt10-1" for="tt10-r1">FORM</label>
    <input type="radio" name="tt10" id="tt10-r2" />
    <label class="tt10b tt10-2" for="tt10-r2">GRID</label>
    <input type="radio" name="tt10" id="tt10-r3" />
    <label class="tt10b tt10-3" for="tt10-r3">TYPE</label>
    <input type="radio" name="tt10" id="tt10-r4" />
    <label class="tt10b tt10-4" for="tt10-r4">HUE</label>
  </nav>
  <div class="tt10p">Composition, weight, balance.</div>
  <div class="tt10p">Modular columns and rows.</div>
  <div class="tt10p">Specimens and pairings.</div>
  <div class="tt10p">Limited primary palette.</div>
</div>
.tt10 {
  min-height: 100vh;
  background: #f5ead0;
  padding: 22px 18px 16px;
  font-family: ui-sans-serif, system-ui, sans-serif;
  width: 100%;
}

.tt10n {
  display: flex;
  gap: 6px;
}

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

.tt10b {
  flex: 1;
  padding: 16px 8px;
  border: 0;
  font: 800 11px/1 ui-monospace, monospace;
  letter-spacing: 0.22em;
  cursor: pointer;
  text-align: center;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s;
}

.tt10-1 {
  background: #c43a32;
  color: #f5ead0;
}

.tt10-2 {
  background: #d4a017;
  color: #1a1a1a;
}

.tt10-3 {
  background: #1a1a1a;
  color: #e8e2d0;
}

.tt10-4 {
  background: #f5ead0;
  color: #1a1a1a;
  box-shadow: inset 0 0 0 1.5px #1a1a1a;
}

.tt10b:hover {
  transform: translateY(-2px);
}

.tt10n input:checked + .tt10b {
  transform: translateY(-6px);
  box-shadow: 0 6px 0 #1a1a1a;
}

.tt10n input:checked + .tt10-4 {
  box-shadow: inset 0 0 0 1.5px #1a1a1a, 0 6px 0 #1a1a1a;
}

.tt10p {
  display: none;
  padding-top: 14px;
  font: 11px/1.6 ui-serif, Georgia, serif;
  color: #3a3a42;
  font-style: italic;
}

.tt10:has(#tt10-r1:checked) .tt10p:nth-of-type(1),
.tt10:has(#tt10-r2:checked) .tt10p:nth-of-type(2),
.tt10:has(#tt10-r3:checked) .tt10p:nth-of-type(3),
.tt10:has(#tt10-r4:checked) .tt10p:nth-of-type(4) {
  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: Bauhaus Block
Source: https://codefronts.com/navigation/css-tabs/bauhaus-block/

Each tab is a distinct primary-color block. Active tab rises 6px and casts a hard offset shadow. Color and geometry as the entire indicator.
## HTML
```html
<div class="tt10">
  <nav class="tt10n">
    <input type="radio" name="tt10" id="tt10-r1" checked />
    <label class="tt10b tt10-1" for="tt10-r1">FORM</label>
    <input type="radio" name="tt10" id="tt10-r2" />
    <label class="tt10b tt10-2" for="tt10-r2">GRID</label>
    <input type="radio" name="tt10" id="tt10-r3" />
    <label class="tt10b tt10-3" for="tt10-r3">TYPE</label>
    <input type="radio" name="tt10" id="tt10-r4" />
    <label class="tt10b tt10-4" for="tt10-r4">HUE</label>
  </nav>
  <div class="tt10p">Composition, weight, balance.</div>
  <div class="tt10p">Modular columns and rows.</div>
  <div class="tt10p">Specimens and pairings.</div>
  <div class="tt10p">Limited primary palette.</div>
</div>
```
## CSS
```css
.tt10 {
  min-height: 100vh;
  background: #f5ead0;
  padding: 22px 18px 16px;
  font-family: ui-sans-serif, system-ui, sans-serif;
  width: 100%;
}

.tt10n {
  display: flex;
  gap: 6px;
}

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

.tt10b {
  flex: 1;
  padding: 16px 8px;
  border: 0;
  font: 800 11px/1 ui-monospace, monospace;
  letter-spacing: 0.22em;
  cursor: pointer;
  text-align: center;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s;
}

.tt10-1 {
  background: #c43a32;
  color: #f5ead0;
}

.tt10-2 {
  background: #d4a017;
  color: #1a1a1a;
}

.tt10-3 {
  background: #1a1a1a;
  color: #e8e2d0;
}

.tt10-4 {
  background: #f5ead0;
  color: #1a1a1a;
  box-shadow: inset 0 0 0 1.5px #1a1a1a;
}

.tt10b:hover {
  transform: translateY(-2px);
}

.tt10n input:checked + .tt10b {
  transform: translateY(-6px);
  box-shadow: 0 6px 0 #1a1a1a;
}

.tt10n input:checked + .tt10-4 {
  box-shadow: inset 0 0 0 1.5px #1a1a1a, 0 6px 0 #1a1a1a;
}

.tt10p {
  display: none;
  padding-top: 14px;
  font: 11px/1.6 ui-serif, Georgia, serif;
  color: #3a3a42;
  font-style: italic;
}

.tt10:has(#tt10-r1:checked) .tt10p:nth-of-type(1),
.tt10:has(#tt10-r2:checked) .tt10p:nth-of-type(2),
.tt10:has(#tt10-r3:checked) .tt10p:nth-of-type(3),
.tt10:has(#tt10-r4:checked) .tt10p:nth-of-type(4) {
  display: block;
}
```

How this works

The .tt10 demo maps each tab to a primary-color block drawn straight from the Bauhaus palette: .tt10-1 is a crimson tile at #c43a32, .tt10-2 is a mustard yellow at #d4a017, .tt10-3 is a graphite black at #1a1a1a, and .tt10-4 is the cream stock at #f5ead0 held in by an inset 1.5px border. There is no shared active state — colour and geometry are the indicator, in the tradition of Herbert Bayer and Josef Albers, where the tile itself does the semantic work.

Interaction is a hidden radio pattern. Four inputs are visually removed with position:absolute; opacity:0, and each label sits in the same flex row as its input. The input:checked + .tt10b selector lifts the active tile 6px on the Y-axis and drops a hard 0 6px 0 #1a1a1a offset shadow underneath — no blur, no softening, the same graphic solidity you see in early Weimar constructivist posters. Hover nudges the tile -2px so the surface stays responsive to a pointer.

Panel routing uses the :has() parent selector. .tt10:has(#tt10-r1:checked) .tt10p:nth-of-type(1) and its three siblings toggle the matching italic serif caption underneath, keeping semantic HTML, WCAG 2.1 focusable radios, and framework-agnostic markup with no dependencies. Copy-paste the four rules and the scoped .tt10 class into any SSR-safe stack.

Make it yours

  • Swap the four tile colours to your own primaries — the recipe holds so long as three tiles carry weight and the fourth (here .tt10-4) is the paper stock kept in by inset 0 0 0 1.5px. Keep the label colour readable against each ground.
  • Change the lift distance by editing translateY(-6px) and matching the shadow to 0 6px 0. The two values must move together — the shadow is the gap the tile has vacated, so any drift reads as a rendering bug.
  • The label typography is 800 11px/1 ui-monospace with letter-spacing:0.22em. Replace with a display grotesque if you have one licensed, but keep the tracking wide — that spacing is what echoes the Bayer Universal lettering.
  • Trade the cream ground #f5ead0 for a warm grey if the tabs sit inside a darker chrome. The panel background .tt10p uses #3a3a42 italic serif, so re-tune caption contrast against any new stock.
  • The overshoot easing cubic-bezier(0.34, 1.56, 0.64, 1) gives the tile a small physical bounce. Flatten to ease-out for a stricter Swiss reading, or extend the duration past 0.3s if the tiles are taller than 16px padding.

Gotchas — read before shipping

  • The hidden radios use position:absolute; opacity:0; pointer-events:none. Do not swap to display:none — that removes them from the tab order and keyboard users lose the ability to arrow-key across the four tiles, breaking WCAG 2.1 focus operability.
  • Every label needs a matching for="tt10-rN" and every input a matching id. Miss one and the whole input:checked + .tt10b chain silently detaches — the tile never lifts and there is no console warning to catch it.
  • The :has() selector powers panel routing and needs Safari 15.4, Chrome 105, Firefox 121 or later. Ship a small @supports not (selector(:has(*))) block that shows all four .tt10p captions if you serve older Firefox ESR channels.
  • Hard offset shadows do not stack cleanly. If you nest the demo inside a wrapper with its own box-shadow, the 0 6px 0 #1a1a1a can clip against sibling tiles. Give .tt10 at least 16px bottom padding as the source does.
  • The .tt10-4 tile carries a combined inset plus offset shadow when checked. If you change the inset stroke width, update both .tt10-4 and input:checked + .tt10-4 together — the two rules are not linked by a variable and drift is easy.
  • The transition is on transform and box-shadow only. Adding a background transition looks smooth in isolation but blurs the constructivist snap between tiles; keep colour changes instant so each tile reads as its own object, not a fade.

Browser support

ChromeSafariFirefoxEdge
105+15.4+121+105+

Panel routing uses :has(). Older browsers still render the tabs, just without caption switching — provide a fallback rule that displays all .tt10p captions if :has() is unsupported.

Techniques used in this demo

Search CodeFronts

Loading…