32 CSS Tab Designs11 / 32
Typewriter
Activating a tab triggers a typewriter animation on the panel: text reveals via max-width steps animation, with a blinking cursor at the end. Pure CSS via radio inputs and steps() timing.
Published
The code
<div class="tt31">
<nav class="tt31n">
<input type="radio" name="tt31" id="tt31-r1" checked />
<label class="tt31b" for="tt31-r1">Greet</label>
<input type="radio" name="tt31" id="tt31-r2" />
<label class="tt31b" for="tt31-r2">Status</label>
<input type="radio" name="tt31" id="tt31-r3" />
<label class="tt31b" for="tt31-r3">Goodbye</label>
</nav>
<div class="tt31body">
<div class="tt31p"><span class="tt31type">Hello, friend.</span></div>
<div class="tt31p"><span class="tt31type">Systems nominal.</span></div>
<div class="tt31p"><span class="tt31type">See you tomorrow.</span></div>
</div>
</div><div class="tt31">
<nav class="tt31n">
<input type="radio" name="tt31" id="tt31-r1" checked />
<label class="tt31b" for="tt31-r1">Greet</label>
<input type="radio" name="tt31" id="tt31-r2" />
<label class="tt31b" for="tt31-r2">Status</label>
<input type="radio" name="tt31" id="tt31-r3" />
<label class="tt31b" for="tt31-r3">Goodbye</label>
</nav>
<div class="tt31body">
<div class="tt31p"><span class="tt31type">Hello, friend.</span></div>
<div class="tt31p"><span class="tt31type">Systems nominal.</span></div>
<div class="tt31p"><span class="tt31type">See you tomorrow.</span></div>
</div>
</div>.tt31 {
background: #1a1625;
padding: 22px 22px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt31n {
display: flex;
gap: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 18px;
min-width: 0;
}
.tt31n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt31b {
flex: 1;
min-width: 0;
padding: 9px 12px;
font: 600 11px/1 ui-monospace, monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.45);
cursor: pointer;
text-align: center;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
}
.tt31b:hover {
color: rgba(255, 255, 255, 0.85);
}
.tt31n input:checked + .tt31b {
color: #fbbf24;
border-bottom-color: #fbbf24;
}
.tt31n input:focus-visible + .tt31b {
outline: 1px dashed #fbbf24;
outline-offset: -3px;
}
.tt31body {
padding: 6px 0;
}
.tt31p {
display: none;
}
.tt31:has(#tt31-r1:checked) .tt31p:nth-of-type(1),
.tt31:has(#tt31-r2:checked) .tt31p:nth-of-type(2),
.tt31:has(#tt31-r3:checked) .tt31p:nth-of-type(3) {
display: block;
}
.tt31type {
display: inline-block;
overflow: hidden;
white-space: nowrap;
font: 14px/1.6 ui-monospace, monospace;
color: #fbbf24;
border-right: 2px solid #fbbf24;
width: 0;
animation: tt31-type 1.4s steps(20) forwards, tt31-blink 0.8s step-end infinite 1.4s;
}
@keyframes tt31-type {
to {
width: 100%;
}
}
@keyframes tt31-blink {
50% {
border-right-color: transparent;
}
}
@media (prefers-reduced-motion: reduce) {
.tt31type {
animation: none;
width: 100%;
border-right-color: transparent;
}
}.tt31 {
background: #1a1625;
padding: 22px 22px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt31n {
display: flex;
gap: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 18px;
min-width: 0;
}
.tt31n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt31b {
flex: 1;
min-width: 0;
padding: 9px 12px;
font: 600 11px/1 ui-monospace, monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.45);
cursor: pointer;
text-align: center;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
}
.tt31b:hover {
color: rgba(255, 255, 255, 0.85);
}
.tt31n input:checked + .tt31b {
color: #fbbf24;
border-bottom-color: #fbbf24;
}
.tt31n input:focus-visible + .tt31b {
outline: 1px dashed #fbbf24;
outline-offset: -3px;
}
.tt31body {
padding: 6px 0;
}
.tt31p {
display: none;
}
.tt31:has(#tt31-r1:checked) .tt31p:nth-of-type(1),
.tt31:has(#tt31-r2:checked) .tt31p:nth-of-type(2),
.tt31:has(#tt31-r3:checked) .tt31p:nth-of-type(3) {
display: block;
}
.tt31type {
display: inline-block;
overflow: hidden;
white-space: nowrap;
font: 14px/1.6 ui-monospace, monospace;
color: #fbbf24;
border-right: 2px solid #fbbf24;
width: 0;
animation: tt31-type 1.4s steps(20) forwards, tt31-blink 0.8s step-end infinite 1.4s;
}
@keyframes tt31-type {
to {
width: 100%;
}
}
@keyframes tt31-blink {
50% {
border-right-color: transparent;
}
}
@media (prefers-reduced-motion: reduce) {
.tt31type {
animation: none;
width: 100%;
border-right-color: transparent;
}
}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: Typewriter
Source: https://codefronts.com/navigation/css-tabs/typewriter/
Activating a tab triggers a typewriter animation on the panel: text reveals via max-width steps animation, with a blinking cursor at the end. Pure CSS via radio inputs and steps() timing.
## HTML
```html
<div class="tt31">
<nav class="tt31n">
<input type="radio" name="tt31" id="tt31-r1" checked />
<label class="tt31b" for="tt31-r1">Greet</label>
<input type="radio" name="tt31" id="tt31-r2" />
<label class="tt31b" for="tt31-r2">Status</label>
<input type="radio" name="tt31" id="tt31-r3" />
<label class="tt31b" for="tt31-r3">Goodbye</label>
</nav>
<div class="tt31body">
<div class="tt31p"><span class="tt31type">Hello, friend.</span></div>
<div class="tt31p"><span class="tt31type">Systems nominal.</span></div>
<div class="tt31p"><span class="tt31type">See you tomorrow.</span></div>
</div>
</div>
```
## CSS
```css
.tt31 {
background: #1a1625;
padding: 22px 22px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt31n {
display: flex;
gap: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 18px;
min-width: 0;
}
.tt31n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt31b {
flex: 1;
min-width: 0;
padding: 9px 12px;
font: 600 11px/1 ui-monospace, monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.45);
cursor: pointer;
text-align: center;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
}
.tt31b:hover {
color: rgba(255, 255, 255, 0.85);
}
.tt31n input:checked + .tt31b {
color: #fbbf24;
border-bottom-color: #fbbf24;
}
.tt31n input:focus-visible + .tt31b {
outline: 1px dashed #fbbf24;
outline-offset: -3px;
}
.tt31body {
padding: 6px 0;
}
.tt31p {
display: none;
}
.tt31:has(#tt31-r1:checked) .tt31p:nth-of-type(1),
.tt31:has(#tt31-r2:checked) .tt31p:nth-of-type(2),
.tt31:has(#tt31-r3:checked) .tt31p:nth-of-type(3) {
display: block;
}
.tt31type {
display: inline-block;
overflow: hidden;
white-space: nowrap;
font: 14px/1.6 ui-monospace, monospace;
color: #fbbf24;
border-right: 2px solid #fbbf24;
width: 0;
animation: tt31-type 1.4s steps(20) forwards, tt31-blink 0.8s step-end infinite 1.4s;
}
@keyframes tt31-type {
to {
width: 100%;
}
}
@keyframes tt31-blink {
50% {
border-right-color: transparent;
}
}
@media (prefers-reduced-motion: reduce) {
.tt31type {
animation: none;
width: 100%;
border-right-color: transparent;
}
}
```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: Typewriter
Source: https://codefronts.com/navigation/css-tabs/typewriter/
Activating a tab triggers a typewriter animation on the panel: text reveals via max-width steps animation, with a blinking cursor at the end. Pure CSS via radio inputs and steps() timing.
## HTML
```html
<div class="tt31">
<nav class="tt31n">
<input type="radio" name="tt31" id="tt31-r1" checked />
<label class="tt31b" for="tt31-r1">Greet</label>
<input type="radio" name="tt31" id="tt31-r2" />
<label class="tt31b" for="tt31-r2">Status</label>
<input type="radio" name="tt31" id="tt31-r3" />
<label class="tt31b" for="tt31-r3">Goodbye</label>
</nav>
<div class="tt31body">
<div class="tt31p"><span class="tt31type">Hello, friend.</span></div>
<div class="tt31p"><span class="tt31type">Systems nominal.</span></div>
<div class="tt31p"><span class="tt31type">See you tomorrow.</span></div>
</div>
</div>
```
## CSS
```css
.tt31 {
background: #1a1625;
padding: 22px 22px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt31n {
display: flex;
gap: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 18px;
min-width: 0;
}
.tt31n input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tt31b {
flex: 1;
min-width: 0;
padding: 9px 12px;
font: 600 11px/1 ui-monospace, monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.45);
cursor: pointer;
text-align: center;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
}
.tt31b:hover {
color: rgba(255, 255, 255, 0.85);
}
.tt31n input:checked + .tt31b {
color: #fbbf24;
border-bottom-color: #fbbf24;
}
.tt31n input:focus-visible + .tt31b {
outline: 1px dashed #fbbf24;
outline-offset: -3px;
}
.tt31body {
padding: 6px 0;
}
.tt31p {
display: none;
}
.tt31:has(#tt31-r1:checked) .tt31p:nth-of-type(1),
.tt31:has(#tt31-r2:checked) .tt31p:nth-of-type(2),
.tt31:has(#tt31-r3:checked) .tt31p:nth-of-type(3) {
display: block;
}
.tt31type {
display: inline-block;
overflow: hidden;
white-space: nowrap;
font: 14px/1.6 ui-monospace, monospace;
color: #fbbf24;
border-right: 2px solid #fbbf24;
width: 0;
animation: tt31-type 1.4s steps(20) forwards, tt31-blink 0.8s step-end infinite 1.4s;
}
@keyframes tt31-type {
to {
width: 100%;
}
}
@keyframes tt31-blink {
50% {
border-right-color: transparent;
}
}
@media (prefers-reduced-motion: reduce) {
.tt31type {
animation: none;
width: 100%;
border-right-color: transparent;
}
}
```How this works
The typewriter tabs demo pairs three radio inputs with sibling labels inside .tt31n, then uses .tt31:has(#tt31-r1:checked) .tt31p:nth-of-type(1) selectors to flip the matching .tt31p panel from display:none to display:block. No JavaScript runs. Because the radios are visually hidden with position:absolute;opacity:0, keyboard tabbing still lands on them and :focus-visible + .tt31b paints a dashed #fbbf24 outline offset -3px into the label, so focus stays legible on the #1a1625 plum background.
Each revealed panel wraps its copy in .tt31type, an inline-block span with overflow:hidden, white-space:nowrap, width:0, and a 2px right border that acts as the caret. Two animations chain: tt31-type uses steps(20) over 1.4s to widen the span to 100%, snapping character by character; then tt31-blink takes over on a 0.8s step-end infinite loop, toggling the border colour so the caret pulses after the line finishes. The steps count is tuned to the sample strings, which is why swapping copy needs an updated step value.
Colour is a two-token system: #fbbf24 amber for active label, underline, caret, and typed body, against rgba(255,255,255,0.45) idle labels. The ui-monospace stack (SF Mono, Menlo) gives every character a fixed advance, so the stepped reveal lands one glyph per step. A prefers-reduced-motion:reduce block cancels both animations, sets width:100%, and hides the caret border, so the panel renders as static amber text on the first paint. Everything is hand-coded CSS with no dependencies, framework-agnostic, and safe to drop into SSR output.
Make it yours
- Retune the reveal cadence by editing the tt31-type step count and duration. Match steps() to the longest string's character count, then tune 1.4s to taste. Longer copy needs proportionally more steps or the tail races.
- Swap the amber accent by replacing #fbbf24 across the checked label, underline, caret border, and typed colour. A green-on-black CRT variant works with #4ade80; keep the plum #1a1625 shell for contrast above 4.5:1.
- Widen the tab bar by removing flex:1 on .tt31b and setting explicit padding:9px 20px. Labels then hug their text length, which reads better once you add a fourth or fifth tab category.
- Change the mono stack to a slab like JetBrains Mono or IBM Plex Mono. Keep letter-spacing:0.1em on the label rail so the uppercase caps stay legible; body copy at 14px does not need extra tracking.
- Add a scanline overlay by giving .tt31 a repeating linear-gradient background layer above the #1a1625 base. Keep opacity under 0.04 so the caret still dominates.
Gotchas — read before shipping
- The demo uses radio inputs with sibling labels, not a real ARIA tab pattern. For assistive tech parity, wrap the panels in role="tabpanel", upgrade labels to role="tab", and manage aria-selected via a small handler, or the reveal reads as generic form input to screen readers.
- The steps(20) value is copy-locked. If a label like Greet is replaced with a 40-character string, the animation still snaps in 20 steps and the tail glyphs whip past. Recompute steps per longest string or split panels into their own keyframes.
- Because .tt31type uses white-space:nowrap, long strings overflow the .tt31body container. On narrow viewports the caret ends off-screen. Wrap the body in overflow-x:auto or clamp the container width to keep the caret in view on 320px screens.
- The @media (prefers-reduced-motion: reduce) block kills the caret border colour, so users on OS-level reduce-motion see no cursor at all. That is intentional for INP-safe delivery, but flag it in your design system: reduced-motion means static text, not a static caret.
- React and Vue re-render can restart the animation if the parent key changes on tab switch. Keep the radios stable across renders by pinning a stable key on the panel array, otherwise every state update replays the 1.4s reveal and lands you in a loop.
- Scoped class names .tt31, .tt31n, .tt31type avoid collisions when copy-pasted next to another tab widget, but the shared name="tt31" on the radios does not. Rename the radio group per instance or two tab sets on the same page will share checked state.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 105+ | 15.4+ | 121+ | 105+ |
The :has() panel selector is the load-bearing feature — supported in Chrome 105, Safari 15.4, and Firefox 121. Older Firefox falls back to hidden panels; wire a small JS toggle for legacy targets.