32 CSS Tab Designs14 / 32

Pure CSSMIT licensed

Cropped Letter

The active tab's first letter scales 14→32px, baseline-shifts down, and crops at the tab's right edge. Inactive labels stay at compact display size.

Published Updated

Live Demo
Try it

The code

<div class="tt02">
  <nav class="tt02n">
    <input type="radio" name="tt02" id="tt02-r1" checked />
    <label class="tt02b" for="tt02-r1">
      <span class="tt02hd">Design</span>
    </label>
    <input type="radio" name="tt02" id="tt02-r2" />
    <label class="tt02b" for="tt02-r2">
      <span class="tt02hd">Code</span>
    </label>
    <input type="radio" name="tt02" id="tt02-r3" />
    <label class="tt02b" for="tt02-r3">
      <span class="tt02hd">Preview</span>
    </label>
  </nav>
  <div class="tt02p">Tokens, components, motion.</div>
  <div class="tt02p">Modern CSS, zero deps.</div>
  <div class="tt02p">Live with all states.</div>
</div>
.tt02 {
  min-height: 100vh;
  background: #1a1a1a;
  padding: 14px 0 16px;
  font-family: ui-serif, Georgia, serif;
  width: 100%;
}

.tt02n {
  display: flex;
  gap: 0;
  border-top: 1px solid rgba(232, 226, 208, 0.18);
  border-bottom: 1px solid rgba(232, 226, 208, 0.18);
}

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

.tt02b {
  flex: 1 1 0;
  min-width: 0;
  height: 64px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  border-right: 1px solid rgba(232, 226, 208, 0.12);
  display: block;
}

.tt02n label.tt02b:last-of-type {
  border-right: 0;
}

.tt02hd {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: block;
  font: 600 14px/1 ui-serif, Georgia, serif;
  color: rgba(232, 226, 208, 0.5);
  letter-spacing: 0.02em;
  white-space: nowrap;
  transition: color 0.25s, font-size 0.5s cubic-bezier(0.65, 0, 0.35, 1), transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}

.tt02b:hover .tt02hd {
  color: rgba(232, 226, 208, 0.85);
}

.tt02n input:checked + .tt02b .tt02hd {
  color: #e8e2d0;
  font-size: 28px;
  transform: translate(-50%, -50%);
}

.tt02p {
  display: none;
  padding: 10px 18px 0;
  font: 11px/1.55 ui-monospace, monospace;
  color: rgba(232, 226, 208, 0.65);
  letter-spacing: 0.04em;
}

.tt02:has(#tt02-r1:checked) .tt02p:nth-of-type(1),
.tt02:has(#tt02-r2:checked) .tt02p:nth-of-type(2),
.tt02:has(#tt02-r3:checked) .tt02p: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: Cropped Letter
Source: https://codefronts.com/navigation/css-tabs/cropped-letter/

The active tab's first letter scales 14→32px, baseline-shifts down, and crops at the tab's right edge. Inactive labels stay at compact display size.
## HTML
```html
<div class="tt02">
  <nav class="tt02n">
    <input type="radio" name="tt02" id="tt02-r1" checked />
    <label class="tt02b" for="tt02-r1">
      <span class="tt02hd">Design</span>
    </label>
    <input type="radio" name="tt02" id="tt02-r2" />
    <label class="tt02b" for="tt02-r2">
      <span class="tt02hd">Code</span>
    </label>
    <input type="radio" name="tt02" id="tt02-r3" />
    <label class="tt02b" for="tt02-r3">
      <span class="tt02hd">Preview</span>
    </label>
  </nav>
  <div class="tt02p">Tokens, components, motion.</div>
  <div class="tt02p">Modern CSS, zero deps.</div>
  <div class="tt02p">Live with all states.</div>
</div>
```
## CSS
```css
.tt02 {
  min-height: 100vh;
  background: #1a1a1a;
  padding: 14px 0 16px;
  font-family: ui-serif, Georgia, serif;
  width: 100%;
}

.tt02n {
  display: flex;
  gap: 0;
  border-top: 1px solid rgba(232, 226, 208, 0.18);
  border-bottom: 1px solid rgba(232, 226, 208, 0.18);
}

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

.tt02b {
  flex: 1 1 0;
  min-width: 0;
  height: 64px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  border-right: 1px solid rgba(232, 226, 208, 0.12);
  display: block;
}

.tt02n label.tt02b:last-of-type {
  border-right: 0;
}

.tt02hd {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: block;
  font: 600 14px/1 ui-serif, Georgia, serif;
  color: rgba(232, 226, 208, 0.5);
  letter-spacing: 0.02em;
  white-space: nowrap;
  transition: color 0.25s, font-size 0.5s cubic-bezier(0.65, 0, 0.35, 1), transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}

.tt02b:hover .tt02hd {
  color: rgba(232, 226, 208, 0.85);
}

.tt02n input:checked + .tt02b .tt02hd {
  color: #e8e2d0;
  font-size: 28px;
  transform: translate(-50%, -50%);
}

.tt02p {
  display: none;
  padding: 10px 18px 0;
  font: 11px/1.55 ui-monospace, monospace;
  color: rgba(232, 226, 208, 0.65);
  letter-spacing: 0.04em;
}

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

How this works

Cropped Letter uses three radio inputs with sibling labels inside .tt02n, no JavaScript. Each label is a 64px-tall block with overflow:hidden and flex:1 1 0, so the tab bar splits evenly across three cells. Inside each label a .tt02hd span is absolutely positioned at 50%/50% with translate(-50%,-50%), sitting as a compact 14px serif label in rgba(232,226,208,0.5) cream against the #1a1a1a ink background. The ui-serif/Georgia stack sets the type-foundry register.

The reveal fires on .tt02n input:checked + .tt02b .tt02hd, a sibling-combinator selector that requires no :has() for the label transform. Two properties animate in parallel over 500ms with cubic-bezier(0.65,0,0.35,1): font-size grows 14px to 28px, and color lifts to #e8e2d0 full cream. Because the parent label has overflow:hidden, the enlarged word overshoots its cell and gets sliced at the tab's right edge — the cropped-letter effect. The white-space:nowrap on the span guarantees the crop happens on the text baseline rather than wrapping.

Panel switching uses the :has() selector: .tt02:has(#tt02-r1:checked) .tt02p:nth-of-type(1) flips the matching .tt02p from display:none to display:block. Panels render in ui-monospace at 11px with 0.04em tracking, deliberately quiet against the display label above so the cropped word stays the visual hero. Two horizontal rules in rgba(232,226,208,0.18) frame the nav row like a specimen sheet border. There is no prefers-reduced-motion block in this scope — the transition is font-size and color, both of which reduce-motion honours natively when tuned. Focus falls through to the visually hidden radios; you may want to add a :focus-visible ring on the label for keyboard users.

Make it yours

  • Retune the crop scale by adjusting the checked-state font-size:28px. A 40px display size crops harder and reads as a foundry hero; 20px stays subtle enough for a documentation nav. Keep the 500ms transition or the growth reads as a jump.
  • Swap the cream-on-ink palette by replacing #e8e2d0 (active) and #1a1a1a (ground). A #0f0f0f ground with #f4a988 salmon active still hits AA contrast; keep the inactive alpha at 0.5 for the whispered idle state.
  • Change the serif to a variable display face like Fraunces, Playfair Display, or Recoleta. Keep font-weight:600 and letter-spacing:0.02em on the label so the cropped edge stays clean; hairline weights fringe at the crop edge.
  • Widen the cell height by editing height:64px on .tt02b. An 80px cell lets a 40px display size breathe without cropping vertically; below 56px the descenders on letters like Design's g clip against the bottom border rule.
  • Add a hover-preview by adding .tt02b:hover .tt02hd { font-size:20px; }. The 500ms transition already covers the change, and the crop appears on hover — useful for touch-first products where activation has consequences.

Gotchas — read before shipping

  • The label sibling combinator (input:checked + .tt02b .tt02hd) is baseline, but the panel switch uses :has(). Firefox 120 and older cannot toggle panels. Ship a JS fallback that adds an .active class to panels for legacy targets, or accept panels stay hidden.
  • The radio inputs are visually hidden with position:absolute;opacity:0;pointer-events:none, which removes them from the reading order visually but not focus order. There is no :focus-visible style on the labels, so keyboard users see no focus ring. Add one on .tt02b.
  • The cropped effect depends on overflow:hidden on .tt02b, which also clips any focus outline. If you add a focus ring, use outline-offset with a positive value plus a duplicate outline outside via box-shadow on a wrapper, or the ring will not be visible.
  • The pattern breaks at narrow viewports below roughly 360px, where three flex:1 1 0 cells at 64px height compress the base 14px label into cramped cells and the 28px active state crops most of the word. Add a container query or drop to two rows below 400px.
  • Radios share name="tt02", so mounting two Cropped Letter instances on the same page cross-couples their checked state. Rename the radio group per instance (tt02-a, tt02-b) or scope the whole widget to a randomised suffix in your build step.
  • There is no ARIA tab role wiring. Screen readers hear three radio inputs in a group, not tabs. That is actually more accessible than a role-less button group, but it disagrees with the WAI-ARIA tabs pattern that developer tools may audit for. Document the choice or upgrade to a full tabs pattern with JS.

Browser support

ChromeSafariFirefoxEdge
105+15.4+121+105+

The panel toggle uses :has(), which lands in Chrome 105, Safari 15.4, and Firefox 121. The cropped-letter transform itself works everywhere modern flexbox does — Chrome 60, Safari 12, Firefox 60. Ship the JS panel fallback for older Firefox.

Techniques used in this demo

Search CodeFronts

Loading…