32 CSS Tab Designs26 / 32

Pure CSSMIT licensed

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

Live Demo
Try it

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

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

Techniques used in this demo

Search CodeFronts

Loading…