32 CSS Tab Designs30 / 32
Folio Fold
Inactive tabs are flat sand-colored rectangles on charcoal. The active tab's top-right corner physically folds down 18px via clip-path with a darker fold-underside triangle, lifts 4px above its siblings, and shows a small fold preview on hover. Inactive tabs preview an 8px fold on hover so the mechanic is discoverable.
Published Updated
The code
<div class="tt18">
<nav class="tt18n">
<input type="radio" name="tt18" id="tt18-r1" checked />
<label class="tt18b" for="tt18-r1">
Page 1
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r2" />
<label class="tt18b" for="tt18-r2">
Page 2
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r3" />
<label class="tt18b" for="tt18-r3">
Page 3
<span class="tt18fold" aria-hidden="true"> </span>
</label>
</nav>
<div class="tt18p">The opening recto.</div>
<div class="tt18p">The continuation.</div>
<div class="tt18p">The closing leaf.</div>
</div><div class="tt18">
<nav class="tt18n">
<input type="radio" name="tt18" id="tt18-r1" checked />
<label class="tt18b" for="tt18-r1">
Page 1
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r2" />
<label class="tt18b" for="tt18-r2">
Page 2
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r3" />
<label class="tt18b" for="tt18-r3">
Page 3
<span class="tt18fold" aria-hidden="true"> </span>
</label>
</nav>
<div class="tt18p">The opening recto.</div>
<div class="tt18p">The continuation.</div>
<div class="tt18p">The closing leaf.</div>
</div>.tt18 {
min-height: 100vh;
background: #25222e;
padding: 26px 22px 22px;
font-family: ui-serif, Georgia, serif;
width: 100%;
}
.tt18n {
display: flex;
gap: 6px;
align-items: stretch;
}
.tt18n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt18b {
position: relative;
flex: 1;
padding: 16px 18px;
border: 0;
background: #d4a574;
font: 600 13px/1 ui-serif, Georgia, serif;
color: rgba(26, 26, 26, 0.55);
cursor: pointer;
text-align: left;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transition: clip-path 0.45s cubic-bezier(0.65, 0, 0.35, 1), color 0.25s, transform 0.3s, box-shadow 0.4s, background 0.25s;
}
.tt18b:hover {
color: rgba(26, 26, 26, 0.9);
background: #dcb084;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
}
.tt18n input:checked + .tt18b {
color: #1a1a1a;
background: #e6c39c;
clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%);
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.45), 0 -2px 0 rgba(255, 255, 255, 0.18) inset;
}
.tt18fold {
position: absolute;
top: 0;
right: 0;
width: 18px;
height: 18px;
background: #1c1a24;
clip-path: polygon(100% 0, 100% 100%, 0 100%);
opacity: 0;
transition: opacity 0.25s 0.2s, width 0.4s cubic-bezier(0.65, 0, 0.35, 1), height 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt18b:hover .tt18fold {
opacity: 0.85;
width: 8px;
height: 8px;
}
.tt18n input:checked + .tt18b .tt18fold {
opacity: 1;
width: 18px;
height: 18px;
}
.tt18p {
display: none;
padding-top: 14px;
font: italic 12px/1.6 ui-serif, Georgia;
color: rgba(212, 165, 116, 0.85);
}
.tt18:has(#tt18-r1:checked) .tt18p:nth-of-type(1),
.tt18:has(#tt18-r2:checked) .tt18p:nth-of-type(2),
.tt18:has(#tt18-r3:checked) .tt18p:nth-of-type(3) {
display: block;
}.tt18 {
min-height: 100vh;
background: #25222e;
padding: 26px 22px 22px;
font-family: ui-serif, Georgia, serif;
width: 100%;
}
.tt18n {
display: flex;
gap: 6px;
align-items: stretch;
}
.tt18n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt18b {
position: relative;
flex: 1;
padding: 16px 18px;
border: 0;
background: #d4a574;
font: 600 13px/1 ui-serif, Georgia, serif;
color: rgba(26, 26, 26, 0.55);
cursor: pointer;
text-align: left;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transition: clip-path 0.45s cubic-bezier(0.65, 0, 0.35, 1), color 0.25s, transform 0.3s, box-shadow 0.4s, background 0.25s;
}
.tt18b:hover {
color: rgba(26, 26, 26, 0.9);
background: #dcb084;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
}
.tt18n input:checked + .tt18b {
color: #1a1a1a;
background: #e6c39c;
clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%);
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.45), 0 -2px 0 rgba(255, 255, 255, 0.18) inset;
}
.tt18fold {
position: absolute;
top: 0;
right: 0;
width: 18px;
height: 18px;
background: #1c1a24;
clip-path: polygon(100% 0, 100% 100%, 0 100%);
opacity: 0;
transition: opacity 0.25s 0.2s, width 0.4s cubic-bezier(0.65, 0, 0.35, 1), height 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt18b:hover .tt18fold {
opacity: 0.85;
width: 8px;
height: 8px;
}
.tt18n input:checked + .tt18b .tt18fold {
opacity: 1;
width: 18px;
height: 18px;
}
.tt18p {
display: none;
padding-top: 14px;
font: italic 12px/1.6 ui-serif, Georgia;
color: rgba(212, 165, 116, 0.85);
}
.tt18:has(#tt18-r1:checked) .tt18p:nth-of-type(1),
.tt18:has(#tt18-r2:checked) .tt18p:nth-of-type(2),
.tt18:has(#tt18-r3:checked) .tt18p: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: Folio Fold
Source: https://codefronts.com/navigation/css-tabs/folio-fold/
Inactive tabs are flat sand-colored rectangles on charcoal. The active tab's top-right corner physically folds down 18px via clip-path with a darker fold-underside triangle, lifts 4px above its siblings, and shows a small fold preview on hover. Inactive tabs preview an 8px fold on hover so the mechanic is discoverable.
## HTML
```html
<div class="tt18">
<nav class="tt18n">
<input type="radio" name="tt18" id="tt18-r1" checked />
<label class="tt18b" for="tt18-r1">
Page 1
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r2" />
<label class="tt18b" for="tt18-r2">
Page 2
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r3" />
<label class="tt18b" for="tt18-r3">
Page 3
<span class="tt18fold" aria-hidden="true"> </span>
</label>
</nav>
<div class="tt18p">The opening recto.</div>
<div class="tt18p">The continuation.</div>
<div class="tt18p">The closing leaf.</div>
</div>
```
## CSS
```css
.tt18 {
min-height: 100vh;
background: #25222e;
padding: 26px 22px 22px;
font-family: ui-serif, Georgia, serif;
width: 100%;
}
.tt18n {
display: flex;
gap: 6px;
align-items: stretch;
}
.tt18n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt18b {
position: relative;
flex: 1;
padding: 16px 18px;
border: 0;
background: #d4a574;
font: 600 13px/1 ui-serif, Georgia, serif;
color: rgba(26, 26, 26, 0.55);
cursor: pointer;
text-align: left;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transition: clip-path 0.45s cubic-bezier(0.65, 0, 0.35, 1), color 0.25s, transform 0.3s, box-shadow 0.4s, background 0.25s;
}
.tt18b:hover {
color: rgba(26, 26, 26, 0.9);
background: #dcb084;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
}
.tt18n input:checked + .tt18b {
color: #1a1a1a;
background: #e6c39c;
clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%);
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.45), 0 -2px 0 rgba(255, 255, 255, 0.18) inset;
}
.tt18fold {
position: absolute;
top: 0;
right: 0;
width: 18px;
height: 18px;
background: #1c1a24;
clip-path: polygon(100% 0, 100% 100%, 0 100%);
opacity: 0;
transition: opacity 0.25s 0.2s, width 0.4s cubic-bezier(0.65, 0, 0.35, 1), height 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt18b:hover .tt18fold {
opacity: 0.85;
width: 8px;
height: 8px;
}
.tt18n input:checked + .tt18b .tt18fold {
opacity: 1;
width: 18px;
height: 18px;
}
.tt18p {
display: none;
padding-top: 14px;
font: italic 12px/1.6 ui-serif, Georgia;
color: rgba(212, 165, 116, 0.85);
}
.tt18:has(#tt18-r1:checked) .tt18p:nth-of-type(1),
.tt18:has(#tt18-r2:checked) .tt18p:nth-of-type(2),
.tt18:has(#tt18-r3:checked) .tt18p: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: Folio Fold
Source: https://codefronts.com/navigation/css-tabs/folio-fold/
Inactive tabs are flat sand-colored rectangles on charcoal. The active tab's top-right corner physically folds down 18px via clip-path with a darker fold-underside triangle, lifts 4px above its siblings, and shows a small fold preview on hover. Inactive tabs preview an 8px fold on hover so the mechanic is discoverable.
## HTML
```html
<div class="tt18">
<nav class="tt18n">
<input type="radio" name="tt18" id="tt18-r1" checked />
<label class="tt18b" for="tt18-r1">
Page 1
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r2" />
<label class="tt18b" for="tt18-r2">
Page 2
<span class="tt18fold" aria-hidden="true"> </span>
</label>
<input type="radio" name="tt18" id="tt18-r3" />
<label class="tt18b" for="tt18-r3">
Page 3
<span class="tt18fold" aria-hidden="true"> </span>
</label>
</nav>
<div class="tt18p">The opening recto.</div>
<div class="tt18p">The continuation.</div>
<div class="tt18p">The closing leaf.</div>
</div>
```
## CSS
```css
.tt18 {
min-height: 100vh;
background: #25222e;
padding: 26px 22px 22px;
font-family: ui-serif, Georgia, serif;
width: 100%;
}
.tt18n {
display: flex;
gap: 6px;
align-items: stretch;
}
.tt18n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt18b {
position: relative;
flex: 1;
padding: 16px 18px;
border: 0;
background: #d4a574;
font: 600 13px/1 ui-serif, Georgia, serif;
color: rgba(26, 26, 26, 0.55);
cursor: pointer;
text-align: left;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transition: clip-path 0.45s cubic-bezier(0.65, 0, 0.35, 1), color 0.25s, transform 0.3s, box-shadow 0.4s, background 0.25s;
}
.tt18b:hover {
color: rgba(26, 26, 26, 0.9);
background: #dcb084;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
}
.tt18n input:checked + .tt18b {
color: #1a1a1a;
background: #e6c39c;
clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%);
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.45), 0 -2px 0 rgba(255, 255, 255, 0.18) inset;
}
.tt18fold {
position: absolute;
top: 0;
right: 0;
width: 18px;
height: 18px;
background: #1c1a24;
clip-path: polygon(100% 0, 100% 100%, 0 100%);
opacity: 0;
transition: opacity 0.25s 0.2s, width 0.4s cubic-bezier(0.65, 0, 0.35, 1), height 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt18b:hover .tt18fold {
opacity: 0.85;
width: 8px;
height: 8px;
}
.tt18n input:checked + .tt18b .tt18fold {
opacity: 1;
width: 18px;
height: 18px;
}
.tt18p {
display: none;
padding-top: 14px;
font: italic 12px/1.6 ui-serif, Georgia;
color: rgba(212, 165, 116, 0.85);
}
.tt18:has(#tt18-r1:checked) .tt18p:nth-of-type(1),
.tt18:has(#tt18-r2:checked) .tt18p:nth-of-type(2),
.tt18:has(#tt18-r3:checked) .tt18p:nth-of-type(3) {
display: block;
}
```How this works
The .tt18 nav is the editorial register — three warm sand tabs at #d4a574 sitting on charcoal #25222e, each with a physical top-right corner fold that mimics a magazine folio being peeled back. The mechanic is clip-path: an inactive tab is a plain rectangle via polygon(0 0, 100% 0, 100% 100%, 0 100%). On hover, the top-right corner clips to an 8px triangle so the fold mechanic is discoverable. On checked, that triangle grows to 18px and the tab lifts 4px on translateY, so the active page reads like it has been lifted off the stack.
The dark triangle you see under the fold is the .tt18fold span — an 18x18px absolutely-positioned element painted in near-charcoal #1c1a24 and clipped via polygon(100% 0, 100% 100%, 0 100%) to form a bottom-right triangle. It sits at opacity:0 until the parent label goes into :hover or its adjacent radio goes :checked, at which point it reveals at 0.85 (hover) or full opacity (active). The 0.2s opacity delay is deliberate — it lets the clip-path finish carving the corner before the underside color shows through, selling the folded-paper illusion.
Panels are wired identically to the specimen card — hidden radios plus a :has()-driven display:block rule per index. The active tab gets an inset white glow at the top edge (0 -2px 0 rgba(255,255,255,0.18) inset) that reads as the paper's grain catching studio light, plus a 16px drop shadow beneath at 45% opacity. The transition uses cubic-bezier(0.65, 0, 0.35, 1) at 0.45s on the clip-path so the fold physically unfolds rather than snapping. Framework-agnostic, semantic HTML radios, WCAG 2.1 keyboard-navigable, INP-safe. Copy-paste the block, scope your parent under .tt18, and the folio renders identically in SSR and on hydration.
Make it yours
- Retune the fold depth by changing the 18px value in clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%) to 12px for a subtle corner peek or 28px for a dramatic magazine-cover reveal. Update the .tt18fold width/height to match, one-to-one.
- Swap the warm sand #d4a574 for cream #f4ecd8, sage #a8b89a, or dusty rose #d4a5a5. The active state lightens to #e6c39c — keep the active tone 15% lighter than the base and the hover tone 5% lighter, and the tactile hierarchy holds.
- Change the fold underside #1c1a24 to match your page background darker by 5-10% luminance. If your .tt18 background shifts to #1a1a1a, drop the fold to #0e0e0e so the triangle reads as the page behind this one rather than a decorative shape.
- The 4px lift on the active tab (transform: translateY(-4px)) sells the mechanic. Increase to 8px for tabs that need to shout, or drop to 2px if the drop shadow at 16px already carries the elevation. Keep the shadow proportional — roughly 4x the lift value.
- For a landscape-magazine variant, remove flex:1 from .tt18b and set explicit widths so tabs read as spread-labels (Recto, Verso, Colophon). Combine with a 32px lift and a heavier fold at 28px and it reads as an art-book table of contents.
Gotchas — read before shipping
- The clip-path transition needs matching vertex counts to interpolate smoothly. Inactive is 4 vertices, hover and active are 5 — some Safari builds handle the mid-animation vertex addition as a snap. Author inactive as a 5-vertex polygon 0 0, 100% 0, 100% 0, 100% 100%, 0 100% if you see snap behavior.
- The .tt18fold is absolutely positioned at top:0; right:0 — if you add padding to the label that pushes content down, the fold stays glued to the label edge, not the content edge. This is correct behavior but surprises developers who expect the fold to track the text.
- The opacity transition on the fold has a 0.2s delay: transition: opacity 0.25s 0.2s. If a user rapidly hovers on and off, the delay stacks perceptually and the fold appears sluggish. Drop the delay to 0.1s for a more responsive feel, or 0 if you want the fold to appear the instant the clip-path starts moving.
- The radio-plus-label pattern doesn't need role="tablist" — native radios already announce as a radio group. Adding role="tab" on the label overwrites the native semantics and breaks NVDA / VoiceOver announcements. Leave the semantic HTML alone; screen readers already know what a radio group is.
- The box-shadow stack on the active state has both an outer shadow (0 8px 16px rgba(0,0,0,0.45)) and an inset highlight (0 -2px 0 rgba(255,255,255,0.18) inset). The inset highlight only reads on the sand tone — if you swap to a lighter cream base, drop the highlight opacity to 0.08 or it will over-glow and lose the paper-grain effect.
- prefers-reduced-motion is not guarded in the shipped CSS — add a media query that sets .tt18b transition to none. Without it, the 450ms clip-path unfold plus 300ms lift is well above the vestibular threshold WCAG 2.1 SC 2.3.3 flags for tier-1 audits.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 105+ | 15.4+ | 121+ | 105+ |
The :has() panel reveal requires Baseline 2023-12. clip-path: polygon() and its transition have shipped in every evergreen browser since 2018 — the folio mechanic itself works back to Chrome 55 and Safari 9.1.