32 CSS Tab Designs26 / 32
Riso Misregistration
Two-color riso print on charcoal stock. Active tab is a solid orange block with a cyan plate misregistered 4px below-left, like a zine pulled with the cyan plate slightly off the orange. No swashes, no icons; just two color plates and a typographic punch.
Published Updated
The code
<div class="tt14">
<nav class="tt14n">
<input type="radio" name="tt14" id="tt14-r1" checked />
<label class="tt14b" for="tt14-r1">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 01</span>
</label>
<input type="radio" name="tt14" id="tt14-r2" />
<label class="tt14b" for="tt14-r2">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 02</span>
</label>
<input type="radio" name="tt14" id="tt14-r3" />
<label class="tt14b" for="tt14-r3">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 03</span>
</label>
<input type="radio" name="tt14" id="tt14-r4" />
<label class="tt14b" for="tt14-r4">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 04</span>
</label>
</nav>
</div><div class="tt14">
<nav class="tt14n">
<input type="radio" name="tt14" id="tt14-r1" checked />
<label class="tt14b" for="tt14-r1">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 01</span>
</label>
<input type="radio" name="tt14" id="tt14-r2" />
<label class="tt14b" for="tt14-r2">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 02</span>
</label>
<input type="radio" name="tt14" id="tt14-r3" />
<label class="tt14b" for="tt14-r3">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 03</span>
</label>
<input type="radio" name="tt14" id="tt14-r4" />
<label class="tt14b" for="tt14-r4">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 04</span>
</label>
</nav>
</div>.tt14 {
background: #0e0f12;
padding: 28px 22px;
font-family: ui-monospace, monospace;
min-height: 100vh;
display: flex;
align-items: center;
width: 100%;
}
.tt14n {
display: flex;
gap: 12px;
flex: 1;
}
.tt14n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt14b {
position: relative;
flex: 1;
border: 0;
background: transparent;
padding: 18px 16px;
font: 800 12px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
isolation: isolate;
transition: color 0.2s;
}
.tt14b:hover {
color: rgba(232, 226, 208, 0.92);
}
.tt14n input:checked + .tt14b {
color: #fdf6e9;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
.tt14plate {
position: absolute;
inset: 0;
background: #ff5e3a;
z-index: -1;
opacity: 0;
transition: opacity 0.05s steps(1, end);
}
.tt14plate::before {
content: "";
position: absolute;
inset: 0;
background: #00a8c8;
transform: translate(-4px, 4px);
mix-blend-mode: multiply;
opacity: 0;
}
.tt14n input:checked + .tt14b .tt14plate {
opacity: 1;
}
.tt14n input:checked + .tt14b .tt14plate::before {
opacity: 0.92;
animation: tt14-plate-shift 0.32s cubic-bezier(0.65, 0, 0.35, 1);
}
@keyframes tt14-plate-shift {
0% {
transform: translate(0, 0);
opacity: 0;
}
55% {
transform: translate(-7px, 7px);
opacity: 0.92;
}
100% {
transform: translate(-4px, 4px);
opacity: 0.92;
}
}
.tt14lbl {
position: relative;
z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
.tt14n input:checked + .tt14b .tt14plate::before {
animation: none;
}
}.tt14 {
background: #0e0f12;
padding: 28px 22px;
font-family: ui-monospace, monospace;
min-height: 100vh;
display: flex;
align-items: center;
width: 100%;
}
.tt14n {
display: flex;
gap: 12px;
flex: 1;
}
.tt14n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt14b {
position: relative;
flex: 1;
border: 0;
background: transparent;
padding: 18px 16px;
font: 800 12px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
isolation: isolate;
transition: color 0.2s;
}
.tt14b:hover {
color: rgba(232, 226, 208, 0.92);
}
.tt14n input:checked + .tt14b {
color: #fdf6e9;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
.tt14plate {
position: absolute;
inset: 0;
background: #ff5e3a;
z-index: -1;
opacity: 0;
transition: opacity 0.05s steps(1, end);
}
.tt14plate::before {
content: "";
position: absolute;
inset: 0;
background: #00a8c8;
transform: translate(-4px, 4px);
mix-blend-mode: multiply;
opacity: 0;
}
.tt14n input:checked + .tt14b .tt14plate {
opacity: 1;
}
.tt14n input:checked + .tt14b .tt14plate::before {
opacity: 0.92;
animation: tt14-plate-shift 0.32s cubic-bezier(0.65, 0, 0.35, 1);
}
@keyframes tt14-plate-shift {
0% {
transform: translate(0, 0);
opacity: 0;
}
55% {
transform: translate(-7px, 7px);
opacity: 0.92;
}
100% {
transform: translate(-4px, 4px);
opacity: 0.92;
}
}
.tt14lbl {
position: relative;
z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
.tt14n input:checked + .tt14b .tt14plate::before {
animation: none;
}
}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: Riso Misregistration
Source: https://codefronts.com/navigation/css-tabs/riso-misregistration/
Two-color riso print on charcoal stock. Active tab is a solid orange block with a cyan plate misregistered 4px below-left, like a zine pulled with the cyan plate slightly off the orange. No swashes, no icons; just two color plates and a typographic punch.
## HTML
```html
<div class="tt14">
<nav class="tt14n">
<input type="radio" name="tt14" id="tt14-r1" checked />
<label class="tt14b" for="tt14-r1">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 01</span>
</label>
<input type="radio" name="tt14" id="tt14-r2" />
<label class="tt14b" for="tt14-r2">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 02</span>
</label>
<input type="radio" name="tt14" id="tt14-r3" />
<label class="tt14b" for="tt14-r3">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 03</span>
</label>
<input type="radio" name="tt14" id="tt14-r4" />
<label class="tt14b" for="tt14-r4">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 04</span>
</label>
</nav>
</div>
```
## CSS
```css
.tt14 {
background: #0e0f12;
padding: 28px 22px;
font-family: ui-monospace, monospace;
min-height: 100vh;
display: flex;
align-items: center;
width: 100%;
}
.tt14n {
display: flex;
gap: 12px;
flex: 1;
}
.tt14n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt14b {
position: relative;
flex: 1;
border: 0;
background: transparent;
padding: 18px 16px;
font: 800 12px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
isolation: isolate;
transition: color 0.2s;
}
.tt14b:hover {
color: rgba(232, 226, 208, 0.92);
}
.tt14n input:checked + .tt14b {
color: #fdf6e9;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
.tt14plate {
position: absolute;
inset: 0;
background: #ff5e3a;
z-index: -1;
opacity: 0;
transition: opacity 0.05s steps(1, end);
}
.tt14plate::before {
content: "";
position: absolute;
inset: 0;
background: #00a8c8;
transform: translate(-4px, 4px);
mix-blend-mode: multiply;
opacity: 0;
}
.tt14n input:checked + .tt14b .tt14plate {
opacity: 1;
}
.tt14n input:checked + .tt14b .tt14plate::before {
opacity: 0.92;
animation: tt14-plate-shift 0.32s cubic-bezier(0.65, 0, 0.35, 1);
}
@keyframes tt14-plate-shift {
0% {
transform: translate(0, 0);
opacity: 0;
}
55% {
transform: translate(-7px, 7px);
opacity: 0.92;
}
100% {
transform: translate(-4px, 4px);
opacity: 0.92;
}
}
.tt14lbl {
position: relative;
z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
.tt14n input:checked + .tt14b .tt14plate::before {
animation: none;
}
}
```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: Riso Misregistration
Source: https://codefronts.com/navigation/css-tabs/riso-misregistration/
Two-color riso print on charcoal stock. Active tab is a solid orange block with a cyan plate misregistered 4px below-left, like a zine pulled with the cyan plate slightly off the orange. No swashes, no icons; just two color plates and a typographic punch.
## HTML
```html
<div class="tt14">
<nav class="tt14n">
<input type="radio" name="tt14" id="tt14-r1" checked />
<label class="tt14b" for="tt14-r1">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 01</span>
</label>
<input type="radio" name="tt14" id="tt14-r2" />
<label class="tt14b" for="tt14-r2">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 02</span>
</label>
<input type="radio" name="tt14" id="tt14-r3" />
<label class="tt14b" for="tt14-r3">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 03</span>
</label>
<input type="radio" name="tt14" id="tt14-r4" />
<label class="tt14b" for="tt14-r4">
<span class="tt14plate" aria-hidden="true"> </span>
<span class="tt14lbl">Issue 04</span>
</label>
</nav>
</div>
```
## CSS
```css
.tt14 {
background: #0e0f12;
padding: 28px 22px;
font-family: ui-monospace, monospace;
min-height: 100vh;
display: flex;
align-items: center;
width: 100%;
}
.tt14n {
display: flex;
gap: 12px;
flex: 1;
}
.tt14n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt14b {
position: relative;
flex: 1;
border: 0;
background: transparent;
padding: 18px 16px;
font: 800 12px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.55);
cursor: pointer;
isolation: isolate;
transition: color 0.2s;
}
.tt14b:hover {
color: rgba(232, 226, 208, 0.92);
}
.tt14n input:checked + .tt14b {
color: #fdf6e9;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
.tt14plate {
position: absolute;
inset: 0;
background: #ff5e3a;
z-index: -1;
opacity: 0;
transition: opacity 0.05s steps(1, end);
}
.tt14plate::before {
content: "";
position: absolute;
inset: 0;
background: #00a8c8;
transform: translate(-4px, 4px);
mix-blend-mode: multiply;
opacity: 0;
}
.tt14n input:checked + .tt14b .tt14plate {
opacity: 1;
}
.tt14n input:checked + .tt14b .tt14plate::before {
opacity: 0.92;
animation: tt14-plate-shift 0.32s cubic-bezier(0.65, 0, 0.35, 1);
}
@keyframes tt14-plate-shift {
0% {
transform: translate(0, 0);
opacity: 0;
}
55% {
transform: translate(-7px, 7px);
opacity: 0.92;
}
100% {
transform: translate(-4px, 4px);
opacity: 0.92;
}
}
.tt14lbl {
position: relative;
z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
.tt14n input:checked + .tt14b .tt14plate::before {
animation: none;
}
}
```How this works
The .tt14 demo simulates a two-plate Risograph pull on charcoal #0e0f12 stock. The active tab draws a solid orange plate at #ff5e3a and a cyan plate at #00a8c8 offset -4px, 4px below and to the left, mixed with mix-blend-mode:multiply. It is the register you see on Colorama, Sigrid Calon or Blackbird Letterpress zines — two ink drums pulled slightly out of alignment, producing the layered colour signature indie print carries.
The layering happens on a single .tt14plate span per label. The span itself is the orange plate, absolutely inset with z-index:-1, and its ::before pseudo-element is the cyan plate translated (-4px, 4px). On check, the plate opacity snaps in via steps(1, end) — a single-frame appearance, no fade — while the cyan pseudo runs @keyframes tt14-plate-shift: it starts at zero translation and opacity 0, overshoots to (-7px, 7px) at 55%, and settles back to (-4px, 4px) at 100%. The overshoot mimics a Riso drum pass where the paper picks up the cyan drum first and then registers to its rest position.
The label text sits at z-index:1 above the plates with #fdf6e9 ink and a text-shadow: 0 1px 0 rgba(0,0,0,0.35) for the letterpress bite. Typography is 800 12px/1 ui-monospace with 0.18em letter-spacing and uppercase — the zine-print convention for issue numbers. A @media (prefers-reduced-motion: reduce) block disables the plate-shift animation but keeps the misregistered rest state. Semantic radios, framework-agnostic, no dependencies, SSR-safe.
Make it yours
- Swap the two plate colours by editing #ff5e3a in .tt14plate background and #00a8c8 in .tt14plate::before. Real Riso duotones pair fluoro-pink with teal or purple with yellow — pick two saturated inks that multiply into a readable third at overlap.
- Change the misregistration offset by editing both translate(-4px, 4px) in the pseudo and the 55% keyframe overshoot. A smaller (-2px, 2px) reads as a tighter press; a wider (-6px, 6px) reads as an amateur pull, which is often the effect you want.
- The mix-blend-mode:multiply is what makes the overlap read as a printed third colour rather than a stacked layer. Do not swap for normal — you lose the whole Riso signature and the tabs read as flat rectangles with a shadow.
- The typographic label is 800 12px/1 ui-monospace uppercase with text-shadow: 0 1px 0 rgba(0,0,0,0.35). Retune the shadow alpha against your new plate colours — on lighter inks the 0.35 reads too heavy and the letters look chiselled rather than printed.
- The isolation:isolate on .tt14b is load-bearing — it creates a stacking context so the z-index:-1 plate sits behind the label but stays inside the button. Remove it and the plate escapes to the parent, breaking the layering.
Gotchas — read before shipping
- The plate uses z-index:-1. Without isolation:isolate on the parent .tt14b, that negative z-index will drop the plate behind the entire .tt14 background and the tab appears empty. Keep both declarations together.
- mix-blend-mode:multiply composites against whatever sits behind. If you drop the .tt14 charcoal ground and place the demo on a white parent, the multiply darkens dramatically and the cyan reads as navy — always test against your actual page ground.
- The steps(1, end) opacity transition on .tt14plate snaps the orange plate in a single frame. Switch to a fade and you lose the printed-object read; the effect depends on the plates arriving as objects, not as tinted regions.
- The prefers-reduced-motion block only disables the pseudo's animation, not the plate opacity. That is correct — the misregistration is the meaning, not the shift — but audit copy will sometimes flag it. Note in your accessibility docs that the rest-state offset is decorative.
- The .tt14plate::before pseudo needs content:"" to render. Delete that and the cyan plate vanishes silently with no console warning; a common paste error when copying only the visible declarations.
- The animation runs on transform and opacity, which stay off the main thread. Avoid adding animated background or filter declarations on the plate — you would drop the composite path and INP will spike on the first tab check on lower-end mobiles.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 88+ | 14+ | 94+ | 88+ |
mix-blend-mode:multiply, isolation:isolate, and negative z-index have been baseline for years. The demo needs no :has() — panel routing is not part of this component.